diff --git a/.editorconfig b/.editorconfig
index 1af917f..12e4b6b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,614 +1,614 @@
-root = true
-# Remove the line below if you want to inherit .editorconfig settings from higher directories
-
-# C# files
-[*.cs]
-
-#### Core EditorConfig Options ####
-
-# Indentation and spacing
-indent_size = 4
-indent_style = space
-tab_width = 4
-
-# New line preferences
-end_of_line = crlf
-insert_final_newline = false
-
-#### .NET Coding Conventions ####
-
-# Organize usings
-dotnet_separate_import_directive_groups = false
-dotnet_sort_system_directives_first = false
-file_header_template =
-
-# this. and Me. preferences
-dotnet_style_qualification_for_event = false:suggestion
-dotnet_style_qualification_for_field = false:suggestion
-dotnet_style_qualification_for_method = false:suggestion
-dotnet_style_qualification_for_property = false:suggestion
-
-# Language keywords vs BCL types preferences
-dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
-dotnet_style_predefined_type_for_member_access = true:suggestion
-
-# Parentheses preferences
-dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:none
-dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
-dotnet_style_parentheses_in_other_operators = never_if_unnecessary
-dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:none
-
-# Modifier preferences
-dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
-
-# Expression-level preferences
-dotnet_style_coalesce_expression = true
-dotnet_style_collection_initializer = true
-dotnet_style_explicit_tuple_names = true
-dotnet_style_null_propagation = true
-dotnet_style_object_initializer = true
-dotnet_style_operator_placement_when_wrapping = beginning_of_line
-dotnet_style_prefer_auto_properties = true
-dotnet_style_prefer_compound_assignment = true
-dotnet_style_prefer_conditional_expression_over_assignment = true
-dotnet_style_prefer_conditional_expression_over_return = true
-dotnet_style_prefer_inferred_anonymous_type_member_names = true
-dotnet_style_prefer_inferred_tuple_names = true
-dotnet_style_prefer_is_null_check_over_reference_equality_method = true
-dotnet_style_prefer_simplified_boolean_expressions = true
-dotnet_style_prefer_simplified_interpolation = true
-
-# Field preferences
-dotnet_style_readonly_field = true
-
-# Parameter preferences
-dotnet_code_quality_unused_parameters = all
-
-# Suppression preferences
-dotnet_remove_unnecessary_suppression_exclusions = none
-
-#### C# Coding Conventions ####
-
-# var preferences
-csharp_style_var_elsewhere = true:silent
-csharp_style_var_for_built_in_types = false:silent
-csharp_style_var_when_type_is_apparent = true:silent
-
-# Expression-bodied members
-csharp_style_expression_bodied_accessors = true:silent
-csharp_style_expression_bodied_constructors = false:silent
-csharp_style_expression_bodied_indexers = true:silent
-csharp_style_expression_bodied_lambdas = true:silent
-csharp_style_expression_bodied_local_functions = false:silent
-csharp_style_expression_bodied_methods = false:silent
-csharp_style_expression_bodied_operators = false:silent
-csharp_style_expression_bodied_properties = true:silent
-
-# Pattern matching preferences
-csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
-csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
-csharp_style_prefer_not_pattern = true:suggestion
-csharp_style_prefer_pattern_matching = true:silent
-csharp_style_prefer_switch_expression = true:silent
-
-# Null-checking preferences
-csharp_style_conditional_delegate_call = true:suggestion
-
-# Modifier preferences
-csharp_prefer_static_local_function = true:suggestion
-csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion
-
-# Code-block preferences
-csharp_prefer_braces = when_multiline:silent
-csharp_prefer_simple_using_statement = true:suggestion
-
-# Expression-level preferences
-csharp_prefer_simple_default_expression = true:suggestion
-csharp_style_deconstructed_variable_declaration = true:suggestion
-csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
-csharp_style_inlined_variable_declaration = true:suggestion
-csharp_style_pattern_local_over_anonymous_function = true
-csharp_style_prefer_index_operator = true:suggestion
-csharp_style_prefer_range_operator = true:suggestion
-csharp_style_throw_expression = true:suggestion
-csharp_style_unused_value_assignment_preference = discard_variable:suggestion
-csharp_style_unused_value_expression_statement_preference = discard_variable:silent
-
-# 'using' directive preferences
-csharp_using_directive_placement = outside_namespace:silent
-
-#### C# Formatting Rules ####
-
-# New line preferences
-csharp_new_line_before_catch = true
-csharp_new_line_before_else = true
-csharp_new_line_before_finally = true
-csharp_new_line_before_members_in_anonymous_types = true
-csharp_new_line_before_members_in_object_initializers = true
-csharp_new_line_before_open_brace = accessors,anonymous_methods,control_blocks,events,indexers,local_functions,methods,properties,types
-csharp_new_line_between_query_expression_clauses = true
-
-# Indentation preferences
-csharp_indent_block_contents = true
-csharp_indent_braces = false
-csharp_indent_case_contents = true
-csharp_indent_case_contents_when_block = false
-csharp_indent_labels = one_less_than_current
-csharp_indent_switch_labels = true
-
-# Space preferences
-csharp_space_after_cast = false
-csharp_space_after_colon_in_inheritance_clause = true
-csharp_space_after_comma = true
-csharp_space_after_dot = false
-csharp_space_after_keywords_in_control_flow_statements = true
-csharp_space_after_semicolon_in_for_statement = true
-csharp_space_around_binary_operators = before_and_after
-csharp_space_around_declaration_statements = false
-csharp_space_before_colon_in_inheritance_clause = true
-csharp_space_before_comma = false
-csharp_space_before_dot = false
-csharp_space_before_open_square_brackets = false
-csharp_space_before_semicolon_in_for_statement = false
-csharp_space_between_empty_square_brackets = false
-csharp_space_between_method_call_empty_parameter_list_parentheses = false
-csharp_space_between_method_call_name_and_opening_parenthesis = false
-csharp_space_between_method_call_parameter_list_parentheses = false
-csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
-csharp_space_between_method_declaration_name_and_open_parenthesis = false
-csharp_space_between_method_declaration_parameter_list_parentheses = false
-csharp_space_between_parentheses = false
-csharp_space_between_square_brackets = false
-
-# Wrapping preferences
-csharp_preserve_single_line_blocks = true
-csharp_preserve_single_line_statements = true
-
-#### Naming styles ####
-
-# Naming rules
-
-dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
-dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
-dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
-
-dotnet_naming_rule.types_should_be_pascal_case.severity = warning
-dotnet_naming_rule.types_should_be_pascal_case.symbols = types
-dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
-
-dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
-dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
-dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
-
-dotnet_naming_rule.struct_should_be_begin_with_s.severity = warning
-dotnet_naming_rule.struct_should_be_begin_with_s.symbols = struct
-dotnet_naming_rule.struct_should_be_begin_with_s.style = begin_with_s
-
-dotnet_naming_rule.property_should_be_pascal_case.severity = warning
-dotnet_naming_rule.property_should_be_pascal_case.symbols = property
-dotnet_naming_rule.property_should_be_pascal_case.style = pascal_case
-
-dotnet_naming_rule.constant_field_should_be_begins_with_c_.severity = warning
-dotnet_naming_rule.constant_field_should_be_begins_with_c_.symbols = constant_field
-dotnet_naming_rule.constant_field_should_be_begins_with_c_.style = begins_with_c_
-
-dotnet_naming_rule.static_field_should_be_starts_with_s_.severity = warning
-dotnet_naming_rule.static_field_should_be_starts_with_s_.symbols = static_field
-dotnet_naming_rule.static_field_should_be_starts_with_s_.style = starts_with_s_
-
-dotnet_naming_rule.private_or_internal_static_field_should_be_starts_with_s_.severity = warning
-dotnet_naming_rule.private_or_internal_static_field_should_be_starts_with_s_.symbols = private_or_internal_static_field
-dotnet_naming_rule.private_or_internal_static_field_should_be_starts_with_s_.style = starts_with_s_
-
-dotnet_naming_rule.public_or_protected_field_should_be_starts_with_m_.severity = warning
-dotnet_naming_rule.public_or_protected_field_should_be_starts_with_m_.symbols = public_or_protected_field
-dotnet_naming_rule.public_or_protected_field_should_be_starts_with_m_.style = starts_with_m_
-
-dotnet_naming_rule.private_or_internal_field_should_be_starts_with_m_.severity = warning
-dotnet_naming_rule.private_or_internal_field_should_be_starts_with_m_.symbols = private_or_internal_field
-dotnet_naming_rule.private_or_internal_field_should_be_starts_with_m_.style = starts_with_m_
-
-dotnet_naming_rule.event_should_be_end_with_event.severity = warning
-dotnet_naming_rule.event_should_be_end_with_event.symbols = event
-dotnet_naming_rule.event_should_be_end_with_event.style = end_with_event
-
-dotnet_naming_rule.delegate_should_be_end_with_delegate.severity = warning
-dotnet_naming_rule.delegate_should_be_end_with_delegate.symbols = delegate
-dotnet_naming_rule.delegate_should_be_end_with_delegate.style = end_with_delegate
-
-dotnet_naming_rule.parameter_should_be_camelcase.severity = warning
-dotnet_naming_rule.parameter_should_be_camelcase.symbols = parameter
-dotnet_naming_rule.parameter_should_be_camelcase.style = camelcase
-
-dotnet_naming_rule.local_should_be_camelcase.severity = warning
-dotnet_naming_rule.local_should_be_camelcase.symbols = local
-dotnet_naming_rule.local_should_be_camelcase.style = camelcase
-
-# Symbol specifications
-
-dotnet_naming_symbols.interface.applicable_kinds = interface
-dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.interface.required_modifiers =
-
-dotnet_naming_symbols.struct.applicable_kinds = struct
-dotnet_naming_symbols.struct.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.struct.required_modifiers =
-
-dotnet_naming_symbols.delegate.applicable_kinds = delegate
-dotnet_naming_symbols.delegate.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.delegate.required_modifiers =
-
-dotnet_naming_symbols.event.applicable_kinds = event
-dotnet_naming_symbols.event.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.event.required_modifiers =
-
-dotnet_naming_symbols.property.applicable_kinds = property
-dotnet_naming_symbols.property.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.property.required_modifiers =
-
-dotnet_naming_symbols.public_or_protected_field.applicable_kinds = field
-dotnet_naming_symbols.public_or_protected_field.applicable_accessibilities = public, protected
-dotnet_naming_symbols.public_or_protected_field.required_modifiers =
-
-dotnet_naming_symbols.static_field.applicable_kinds = field
-dotnet_naming_symbols.static_field.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.static_field.required_modifiers = static
-
-dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
-dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
-dotnet_naming_symbols.private_or_internal_field.required_modifiers =
-
-dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field
-dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private, private_protected
-dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static
-
-dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
-dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.types.required_modifiers =
-
-dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
-dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.non_field_members.required_modifiers =
-
-dotnet_naming_symbols.parameter.applicable_kinds = parameter
-dotnet_naming_symbols.parameter.applicable_accessibilities = *
-dotnet_naming_symbols.parameter.required_modifiers =
-
-dotnet_naming_symbols.local.applicable_kinds = local
-dotnet_naming_symbols.local.applicable_accessibilities = local
-dotnet_naming_symbols.local.required_modifiers =
-
-dotnet_naming_symbols.constant_field.applicable_kinds = field
-dotnet_naming_symbols.constant_field.applicable_accessibilities = *
-dotnet_naming_symbols.constant_field.required_modifiers = const
-
-# Naming styles
-
-dotnet_naming_style.pascal_case.required_prefix =
-dotnet_naming_style.pascal_case.required_suffix =
-dotnet_naming_style.pascal_case.word_separator =
-dotnet_naming_style.pascal_case.capitalization = pascal_case
-
-dotnet_naming_style.begins_with_i.required_prefix = I
-dotnet_naming_style.begins_with_i.required_suffix =
-dotnet_naming_style.begins_with_i.word_separator =
-dotnet_naming_style.begins_with_i.capitalization = pascal_case
-
-dotnet_naming_style.begin_with_s.required_prefix = S
-dotnet_naming_style.begin_with_s.required_suffix =
-dotnet_naming_style.begin_with_s.word_separator =
-dotnet_naming_style.begin_with_s.capitalization = pascal_case
-
-dotnet_naming_style.starts_with_m_.required_prefix = m_
-dotnet_naming_style.starts_with_m_.required_suffix =
-dotnet_naming_style.starts_with_m_.word_separator =
-dotnet_naming_style.starts_with_m_.capitalization = pascal_case
-
-dotnet_naming_style.starts_with_s_.required_prefix = s_
-dotnet_naming_style.starts_with_s_.required_suffix =
-dotnet_naming_style.starts_with_s_.word_separator =
-dotnet_naming_style.starts_with_s_.capitalization = pascal_case
-
-dotnet_naming_style.end_with_delegate.required_prefix =
-dotnet_naming_style.end_with_delegate.required_suffix = Delegate
-dotnet_naming_style.end_with_delegate.word_separator =
-dotnet_naming_style.end_with_delegate.capitalization = pascal_case
-
-dotnet_naming_style.end_with_event.required_prefix =
-dotnet_naming_style.end_with_event.required_suffix = Event
-dotnet_naming_style.end_with_event.word_separator =
-dotnet_naming_style.end_with_event.capitalization = pascal_case
-
-dotnet_naming_style.camelcase.required_prefix =
-dotnet_naming_style.camelcase.required_suffix =
-dotnet_naming_style.camelcase.word_separator =
-dotnet_naming_style.camelcase.capitalization = camel_case
-
-dotnet_naming_style.begins_with_c_.required_prefix = c_
-dotnet_naming_style.begins_with_c_.required_suffix =
-dotnet_naming_style.begins_with_c_.word_separator =
-dotnet_naming_style.begins_with_c_.capitalization = pascal_case
-
-# ReSharper properties
-resharper_align_linq_query = true
-resharper_align_multiline_argument = true
-resharper_align_multiline_calls_chain = true
-resharper_align_multiline_extends_list = true
-resharper_align_multiline_for_stmt = true
-resharper_align_multline_type_parameter_constrains = true
-resharper_align_multline_type_parameter_list = true
-resharper_align_tuple_components = true
-resharper_allow_far_alignment = false
-resharper_apply_auto_detected_rules = false
-resharper_blank_lines_after_control_transfer_statements = 1
-resharper_blank_lines_after_file_scoped_namespace_directive = 1
-resharper_blank_lines_around_single_line_type = 0
-resharper_blank_lines_before_multiline_statements = 0
-resharper_braces_for_fixed = required
-resharper_braces_for_lock = required
-resharper_braces_for_using = required
-resharper_csharp_align_multiline_parameter = true
-resharper_csharp_align_multiple_declaration = true
-resharper_csharp_allow_far_alignment = true
-resharper_csharp_blank_lines_around_field = 0
-resharper_csharp_extra_spaces = remove_all
-resharper_csharp_insert_final_newline = false
-resharper_csharp_keep_blank_lines_in_code = 1
-resharper_csharp_keep_blank_lines_in_declarations = 1
-resharper_csharp_max_line_length = 500
-resharper_csharp_naming_rule.constants = c_ + AaBb
-resharper_csharp_naming_rule.event = AaBb
-resharper_csharp_naming_rule.interfaces = I + AaBb
-resharper_csharp_naming_rule.local_constants = aaBb
-resharper_csharp_naming_rule.method = AaBb
-resharper_csharp_naming_rule.private_constants = c_ + AaBb
-resharper_csharp_naming_rule.private_instance_fields = m_ + AaBb
-resharper_csharp_naming_rule.private_static_fields = s_ + AaBb
-resharper_csharp_naming_rule.private_static_readonly = s_ + AaBb
-resharper_csharp_naming_rule.property = AaBb
-resharper_csharp_naming_rule.public_fields = m_ + AaBb
-resharper_csharp_naming_rule.static_readonly = s_ + AaBb
-resharper_csharp_naming_rule.types_and_namespaces = AaBb
-resharper_csharp_outdent_commas = true
-resharper_csharp_outdent_dots = false
-resharper_csharp_stick_comment = false
-resharper_csharp_use_indent_from_vs = true
-resharper_csharp_wrap_extends_list_style = chop_if_long
-resharper_csharp_wrap_lines = false
-resharper_csharp_wrap_parameters_style = chop_if_long
-resharper_csharp_wrap_ternary_expr_style = wrap_if_long
-resharper_csharp_wrap_chained_method_calls = chop_if_long
-resharper_extra_spaces = remove_all
-resharper_for_built_in_types = use_var
-resharper_indent_preprocessor_region = no_indent
-resharper_instance_members_qualify_declared_in =
-resharper_int_align_switch_expressions = false
-resharper_int_align_switch_sections = false
-resharper_keep_existing_arrangement = true
-resharper_keep_existing_attribute_arrangement = true
-resharper_keep_existing_switch_expression_arrangement = true
-resharper_max_attribute_length_for_same_line = 0
-resharper_max_initializer_elements_on_line = 1
-resharper_nested_ternary_style = compact
-resharper_object_creation_when_type_not_evident = target_typed
-resharper_outdent_binary_ops = true
-resharper_place_constructor_initializer_on_same_line = false
-resharper_place_method_attribute_on_same_line = if_owner_is_single_line
-resharper_place_simple_case_statement_on_same_line = true
-resharper_place_simple_embedded_statement_on_same_line = false
-resharper_place_simple_initializer_on_single_line = true
-resharper_place_simple_property_pattern_on_single_line = true
-resharper_place_type_attribute_on_same_line = if_owner_is_single_line
-resharper_use_heuristics_for_body_style = true
-resharper_use_indent_from_vs = true
-resharper_space_before_new_parentheses = false
-resharper_use_roslyn_logic_for_evident_types = true
-
-# IDE0090: Use 'new(...)'
-dotnet_diagnostic.ide0090.severity = silent
-dotnet_diagnostic.ide0110.severity = suggestion
-dotnet_diagnostic.ide0058.severity = suggestion
-dotnet_diagnostic.ide0059.severity = suggestion
-csharp_style_namespace_declarations = file_scoped:none
-csharp_style_prefer_null_check_over_type_check = true:suggestion
-csharp_style_prefer_local_over_anonymous_function = true:suggestion
-csharp_style_prefer_tuple_swap = true:suggestion
-csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
-csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
-csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
-csharp_style_prefer_extended_property_pattern = true:suggestion
-
-# Microsoft .NET properties
-dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.constants_rule.severity = none
-dotnet_naming_rule.constants_rule.style = begins_with_c_
-dotnet_naming_rule.constants_rule.symbols = constants_symbols
-dotnet_naming_rule.event_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.event_rule.severity = none
-dotnet_naming_rule.event_rule.style = pascal_case
-dotnet_naming_rule.event_rule.symbols = event_symbols
-dotnet_naming_rule.interfaces_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.interfaces_rule.severity = none
-dotnet_naming_rule.interfaces_rule.style = begins_with_i
-dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols
-dotnet_naming_rule.local_constants_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.local_constants_rule.severity = none
-dotnet_naming_rule.local_constants_rule.style = camelcase
-dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols
-dotnet_naming_rule.local_should_be_camelcase.import_to_resharper = as_predefined
-dotnet_naming_rule.method_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.method_rule.severity = none
-dotnet_naming_rule.method_rule.style = pascal_case
-dotnet_naming_rule.method_rule.symbols = method_symbols
-dotnet_naming_rule.non_field_members_should_be_pascal_case.import_to_resharper = True
-dotnet_naming_rule.non_field_members_should_be_pascal_case.resharper_description = non_field_members_should_be_pascal_case
-dotnet_naming_rule.non_field_members_should_be_pascal_case.resharper_guid = 44ab0b2d-34cc-42db-a691-0c646821e2e4
-dotnet_naming_rule.parameter_should_be_camelcase.import_to_resharper = as_predefined
-dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.private_constants_rule.severity = none
-dotnet_naming_rule.private_constants_rule.style = begins_with_c_
-dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
-dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.private_instance_fields_rule.severity = none
-dotnet_naming_rule.private_instance_fields_rule.style = starts_with_m_
-dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
-dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.private_static_fields_rule.severity = none
-dotnet_naming_rule.private_static_fields_rule.style = starts_with_s_
-dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
-dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.private_static_readonly_rule.severity = none
-dotnet_naming_rule.private_static_readonly_rule.style = starts_with_s_
-dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
-dotnet_naming_rule.property_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.property_rule.severity = none
-dotnet_naming_rule.property_rule.style = pascal_case
-dotnet_naming_rule.property_rule.symbols = property_symbols
-dotnet_naming_rule.public_fields_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.public_fields_rule.severity = none
-dotnet_naming_rule.public_fields_rule.style = starts_with_m_
-dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols
-dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.static_readonly_rule.severity = none
-dotnet_naming_rule.static_readonly_rule.style = starts_with_s_
-dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols
-dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper = as_predefined
-dotnet_naming_rule.types_and_namespaces_rule.severity = none
-dotnet_naming_rule.types_and_namespaces_rule.style = pascal_case
-dotnet_naming_rule.types_and_namespaces_rule.symbols = types_and_namespaces_symbols
-dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
-dotnet_naming_symbols.constants_symbols.applicable_kinds = field
-dotnet_naming_symbols.constants_symbols.required_modifiers = const
-dotnet_naming_symbols.event_symbols.applicable_accessibilities = *
-dotnet_naming_symbols.event_symbols.applicable_kinds = event
-dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = *
-dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface
-dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = *
-dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local
-dotnet_naming_symbols.local_constants_symbols.required_modifiers = const
-dotnet_naming_symbols.method_symbols.applicable_accessibilities = *
-dotnet_naming_symbols.method_symbols.applicable_kinds = method
-dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
-dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
-dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
-dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
-dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
-dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
-dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
-dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
-dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
-dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
-dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
-dotnet_naming_symbols.property_symbols.applicable_accessibilities = *
-dotnet_naming_symbols.property_symbols.applicable_kinds = property
-dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
-dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field
-dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
-dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field
-dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static,readonly
-dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities = *
-dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace,class,struct,enum,delegate
-
-# ReSharper inspection severities
-resharper_arrange_accessor_owner_body_highlighting = suggestion
-resharper_arrange_default_value_when_type_evident_highlighting = hint
-resharper_arrange_object_creation_when_type_evident_highlighting = hint
-resharper_arrange_redundant_parentheses_highlighting = hint
-resharper_arrange_this_qualifier_highlighting = hint
-resharper_arrange_type_member_modifiers_highlighting = hint
-resharper_arrange_type_modifiers_highlighting = hint
-resharper_built_in_type_reference_style_for_member_access_highlighting = hint
-resharper_built_in_type_reference_style_highlighting = hint
-resharper_comment_typo_highlighting = hint
-resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting = none
-resharper_identifier_typo_highlighting = hint
-resharper_inconsistent_naming_highlighting = none
-resharper_json_validation_failed_highlighting = warning
-resharper_member_can_be_private_global_highlighting = hint
-resharper_member_can_be_private_local_highlighting = hint
-resharper_member_can_be_protected_global_highlighting = hint
-resharper_member_can_be_protected_local_highlighting = hint
-resharper_redundant_base_qualifier_highlighting = warning
-resharper_remove_redundant_braces_highlighting = suggestion
-resharper_string_literal_typo_highlighting = hint
-resharper_suggest_var_or_type_built_in_types_highlighting = hint
-resharper_suggest_var_or_type_elsewhere_highlighting = hint
-resharper_suggest_var_or_type_simple_types_highlighting = hint
-resharper_unused_auto_property_accessor_global_highlighting = hint
-resharper_unused_auto_property_accessor_local_highlighting = hint
-resharper_unused_member_global_highlighting = hint
-csharp_style_prefer_method_group_conversion = true:suggestion
-csharp_style_prefer_top_level_statements = true:silent
-dotnet_diagnostic.VSTHRD001.severity = none
-dotnet_diagnostic.VSTHRD105.severity = silent
-csharp_style_prefer_primary_constructors = true:suggestion
-csharp_prefer_system_threading_lock = true:suggestion
-dotnet_diagnostic.VSTHRD012.severity = none
-dotnet_diagnostic.VSTHRD003.severity = none
-
-[*.{cs,vb}]
-dotnet_diagnostic.ca1822.severity = silent
-dotnet_diagnostic.ca1305.severity = suggestion
-dotnet_diagnostic.IDE0079.severity = silent
-dotnet_diagnostic.IDE0270.severity = silent
-dotnet_style_qualification_for_field = false:suggestion
-dotnet_style_qualification_for_property = false:suggestion
-dotnet_style_qualification_for_method = false:suggestion
-dotnet_style_qualification_for_event = false:suggestion
-dotnet_diagnostic.ca2215.severity = warning
-dotnet_diagnostic.ca1001.severity = suggestion
-dotnet_style_operator_placement_when_wrapping = beginning_of_line
-tab_width = 4
-indent_size = 4
-end_of_line = crlf
-dotnet_style_coalesce_expression = true:suggestion
-dotnet_style_null_propagation = true:suggestion
-dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
-dotnet_style_prefer_auto_properties = true:silent
-dotnet_style_object_initializer = true:suggestion
-dotnet_style_collection_initializer = true:suggestion
-dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
-dotnet_style_prefer_conditional_expression_over_assignment = true:silent
-dotnet_style_prefer_conditional_expression_over_return = true:silent
-dotnet_style_explicit_tuple_names = true:suggestion
-dotnet_style_prefer_inferred_tuple_names = true:suggestion
-dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
-dotnet_style_prefer_simplified_interpolation = true:suggestion
-dotnet_style_prefer_compound_assignment = true:suggestion
-dotnet_style_namespace_match_folder = true:suggestion
-dotnet_style_readonly_field = true:suggestion
-dotnet_style_predefined_type_for_locals_parameters_members = true:silent
-dotnet_style_predefined_type_for_member_access = true:silent
-dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
-dotnet_style_allow_multiple_blank_lines_experimental = true:silent
-dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
-dotnet_code_quality_unused_parameters = all:suggestion
-dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
-
-[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}]
-indent_style = space
-indent_size = 4
-tab_width = 4
-
-[*.{appxmanifest,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cshtml,csproj,cu,cuh,cxx,dbml,discomap,dtd,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,jsproj,lsproj,mpp,mq4,mq5,mqh,njsproj,nuspec,proj,props,proto,razor,resw,resx,StyleCop,targets,tasks,tpp,usf,ush,vbproj,xml,xsd}]
-indent_style = space
-indent_size = 4
-tab_width = 4
-
-[*.{asax,ascx,aspx,axaml,cs,css,js,jsx,master,paml,skin,ts,tsx,vb,xaml,xamlx,xoml}]
-indent_style = space
-indent_size = 4
-tab_width = 4
-dotnet_diagnostic.CA1816.severity = silent
-dotnet_diagnostic.HAA0501.severity = silent
-dotnet_diagnostic.HAA0502.severity = silent
-dotnet_style_prefer_collection_expression = true:suggestion
-dotnet_diagnostic.RS2008.severity = none
-
-[*.{json,resjson}]
-indent_style = space
-indent_size = 4
-tab_width = 4
+root = true
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = false
+file_header_template =
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:none
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:none
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true
+dotnet_style_collection_initializer = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_null_propagation = true
+dotnet_style_object_initializer = true
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true
+dotnet_style_prefer_compound_assignment = true
+dotnet_style_prefer_conditional_expression_over_assignment = true
+dotnet_style_prefer_conditional_expression_over_return = true
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_style_prefer_simplified_interpolation = true
+
+# Field preferences
+dotnet_style_readonly_field = true
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = true:silent
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = true:silent
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_prefer_switch_expression = true:silent
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+csharp_prefer_static_local_function = true:suggestion
+csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion
+
+# Code-block preferences
+csharp_prefer_braces = when_multiline:silent
+csharp_prefer_simple_using_statement = true:suggestion
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:silent
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = accessors,anonymous_methods,control_blocks,events,indexers,local_functions,methods,properties,types
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = warning
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.struct_should_be_begin_with_s.severity = warning
+dotnet_naming_rule.struct_should_be_begin_with_s.symbols = struct
+dotnet_naming_rule.struct_should_be_begin_with_s.style = begin_with_s
+
+dotnet_naming_rule.property_should_be_pascal_case.severity = warning
+dotnet_naming_rule.property_should_be_pascal_case.symbols = property
+dotnet_naming_rule.property_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.constant_field_should_be_begins_with_c_.severity = warning
+dotnet_naming_rule.constant_field_should_be_begins_with_c_.symbols = constant_field
+dotnet_naming_rule.constant_field_should_be_begins_with_c_.style = begins_with_c_
+
+dotnet_naming_rule.static_field_should_be_starts_with_s_.severity = warning
+dotnet_naming_rule.static_field_should_be_starts_with_s_.symbols = static_field
+dotnet_naming_rule.static_field_should_be_starts_with_s_.style = starts_with_s_
+
+dotnet_naming_rule.private_or_internal_static_field_should_be_starts_with_s_.severity = warning
+dotnet_naming_rule.private_or_internal_static_field_should_be_starts_with_s_.symbols = private_or_internal_static_field
+dotnet_naming_rule.private_or_internal_static_field_should_be_starts_with_s_.style = starts_with_s_
+
+dotnet_naming_rule.public_or_protected_field_should_be_starts_with_m_.severity = warning
+dotnet_naming_rule.public_or_protected_field_should_be_starts_with_m_.symbols = public_or_protected_field
+dotnet_naming_rule.public_or_protected_field_should_be_starts_with_m_.style = starts_with_m_
+
+dotnet_naming_rule.private_or_internal_field_should_be_starts_with_m_.severity = warning
+dotnet_naming_rule.private_or_internal_field_should_be_starts_with_m_.symbols = private_or_internal_field
+dotnet_naming_rule.private_or_internal_field_should_be_starts_with_m_.style = starts_with_m_
+
+dotnet_naming_rule.event_should_be_end_with_event.severity = warning
+dotnet_naming_rule.event_should_be_end_with_event.symbols = event
+dotnet_naming_rule.event_should_be_end_with_event.style = end_with_event
+
+dotnet_naming_rule.delegate_should_be_end_with_delegate.severity = warning
+dotnet_naming_rule.delegate_should_be_end_with_delegate.symbols = delegate
+dotnet_naming_rule.delegate_should_be_end_with_delegate.style = end_with_delegate
+
+dotnet_naming_rule.parameter_should_be_camelcase.severity = warning
+dotnet_naming_rule.parameter_should_be_camelcase.symbols = parameter
+dotnet_naming_rule.parameter_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.local_should_be_camelcase.severity = warning
+dotnet_naming_rule.local_should_be_camelcase.symbols = local
+dotnet_naming_rule.local_should_be_camelcase.style = camelcase
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.struct.applicable_kinds = struct
+dotnet_naming_symbols.struct.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.struct.required_modifiers =
+
+dotnet_naming_symbols.delegate.applicable_kinds = delegate
+dotnet_naming_symbols.delegate.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.delegate.required_modifiers =
+
+dotnet_naming_symbols.event.applicable_kinds = event
+dotnet_naming_symbols.event.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.event.required_modifiers =
+
+dotnet_naming_symbols.property.applicable_kinds = property
+dotnet_naming_symbols.property.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.property.required_modifiers =
+
+dotnet_naming_symbols.public_or_protected_field.applicable_kinds = field
+dotnet_naming_symbols.public_or_protected_field.applicable_accessibilities = public, protected
+dotnet_naming_symbols.public_or_protected_field.required_modifiers =
+
+dotnet_naming_symbols.static_field.applicable_kinds = field
+dotnet_naming_symbols.static_field.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.static_field.required_modifiers = static
+
+dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
+dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
+dotnet_naming_symbols.private_or_internal_field.required_modifiers =
+
+dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field
+dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private, private_protected
+dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+dotnet_naming_symbols.parameter.applicable_kinds = parameter
+dotnet_naming_symbols.parameter.applicable_accessibilities = *
+dotnet_naming_symbols.parameter.required_modifiers =
+
+dotnet_naming_symbols.local.applicable_kinds = local
+dotnet_naming_symbols.local.applicable_accessibilities = local
+dotnet_naming_symbols.local.required_modifiers =
+
+dotnet_naming_symbols.constant_field.applicable_kinds = field
+dotnet_naming_symbols.constant_field.applicable_accessibilities = *
+dotnet_naming_symbols.constant_field.required_modifiers = const
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.begin_with_s.required_prefix = S
+dotnet_naming_style.begin_with_s.required_suffix =
+dotnet_naming_style.begin_with_s.word_separator =
+dotnet_naming_style.begin_with_s.capitalization = pascal_case
+
+dotnet_naming_style.starts_with_m_.required_prefix = m_
+dotnet_naming_style.starts_with_m_.required_suffix =
+dotnet_naming_style.starts_with_m_.word_separator =
+dotnet_naming_style.starts_with_m_.capitalization = pascal_case
+
+dotnet_naming_style.starts_with_s_.required_prefix = s_
+dotnet_naming_style.starts_with_s_.required_suffix =
+dotnet_naming_style.starts_with_s_.word_separator =
+dotnet_naming_style.starts_with_s_.capitalization = pascal_case
+
+dotnet_naming_style.end_with_delegate.required_prefix =
+dotnet_naming_style.end_with_delegate.required_suffix = Delegate
+dotnet_naming_style.end_with_delegate.word_separator =
+dotnet_naming_style.end_with_delegate.capitalization = pascal_case
+
+dotnet_naming_style.end_with_event.required_prefix =
+dotnet_naming_style.end_with_event.required_suffix = Event
+dotnet_naming_style.end_with_event.word_separator =
+dotnet_naming_style.end_with_event.capitalization = pascal_case
+
+dotnet_naming_style.camelcase.required_prefix =
+dotnet_naming_style.camelcase.required_suffix =
+dotnet_naming_style.camelcase.word_separator =
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_style.begins_with_c_.required_prefix = c_
+dotnet_naming_style.begins_with_c_.required_suffix =
+dotnet_naming_style.begins_with_c_.word_separator =
+dotnet_naming_style.begins_with_c_.capitalization = pascal_case
+
+# ReSharper properties
+resharper_align_linq_query = true
+resharper_align_multiline_argument = true
+resharper_align_multiline_calls_chain = true
+resharper_align_multiline_extends_list = true
+resharper_align_multiline_for_stmt = true
+resharper_align_multline_type_parameter_constrains = true
+resharper_align_multline_type_parameter_list = true
+resharper_align_tuple_components = true
+resharper_allow_far_alignment = false
+resharper_apply_auto_detected_rules = false
+resharper_blank_lines_after_control_transfer_statements = 1
+resharper_blank_lines_after_file_scoped_namespace_directive = 1
+resharper_blank_lines_around_single_line_type = 0
+resharper_blank_lines_before_multiline_statements = 0
+resharper_braces_for_fixed = required
+resharper_braces_for_lock = required
+resharper_braces_for_using = required
+resharper_csharp_align_multiline_parameter = true
+resharper_csharp_align_multiple_declaration = true
+resharper_csharp_allow_far_alignment = true
+resharper_csharp_blank_lines_around_field = 0
+resharper_csharp_extra_spaces = remove_all
+resharper_csharp_insert_final_newline = false
+resharper_csharp_keep_blank_lines_in_code = 1
+resharper_csharp_keep_blank_lines_in_declarations = 1
+resharper_csharp_max_line_length = 500
+resharper_csharp_naming_rule.constants = c_ + AaBb
+resharper_csharp_naming_rule.event = AaBb
+resharper_csharp_naming_rule.interfaces = I + AaBb
+resharper_csharp_naming_rule.local_constants = aaBb
+resharper_csharp_naming_rule.method = AaBb
+resharper_csharp_naming_rule.private_constants = c_ + AaBb
+resharper_csharp_naming_rule.private_instance_fields = m_ + AaBb
+resharper_csharp_naming_rule.private_static_fields = s_ + AaBb
+resharper_csharp_naming_rule.private_static_readonly = s_ + AaBb
+resharper_csharp_naming_rule.property = AaBb
+resharper_csharp_naming_rule.public_fields = m_ + AaBb
+resharper_csharp_naming_rule.static_readonly = s_ + AaBb
+resharper_csharp_naming_rule.types_and_namespaces = AaBb
+resharper_csharp_outdent_commas = true
+resharper_csharp_outdent_dots = false
+resharper_csharp_stick_comment = false
+resharper_csharp_use_indent_from_vs = true
+resharper_csharp_wrap_extends_list_style = chop_if_long
+resharper_csharp_wrap_lines = false
+resharper_csharp_wrap_parameters_style = chop_if_long
+resharper_csharp_wrap_ternary_expr_style = wrap_if_long
+resharper_csharp_wrap_chained_method_calls = chop_if_long
+resharper_extra_spaces = remove_all
+resharper_for_built_in_types = use_var
+resharper_indent_preprocessor_region = no_indent
+resharper_instance_members_qualify_declared_in =
+resharper_int_align_switch_expressions = false
+resharper_int_align_switch_sections = false
+resharper_keep_existing_arrangement = true
+resharper_keep_existing_attribute_arrangement = true
+resharper_keep_existing_switch_expression_arrangement = true
+resharper_max_attribute_length_for_same_line = 0
+resharper_max_initializer_elements_on_line = 1
+resharper_nested_ternary_style = compact
+resharper_object_creation_when_type_not_evident = target_typed
+resharper_outdent_binary_ops = true
+resharper_place_constructor_initializer_on_same_line = false
+resharper_place_method_attribute_on_same_line = if_owner_is_single_line
+resharper_place_simple_case_statement_on_same_line = true
+resharper_place_simple_embedded_statement_on_same_line = false
+resharper_place_simple_initializer_on_single_line = true
+resharper_place_simple_property_pattern_on_single_line = true
+resharper_place_type_attribute_on_same_line = if_owner_is_single_line
+resharper_use_heuristics_for_body_style = true
+resharper_use_indent_from_vs = true
+resharper_space_before_new_parentheses = false
+resharper_use_roslyn_logic_for_evident_types = true
+
+# IDE0090: Use 'new(...)'
+dotnet_diagnostic.ide0090.severity = silent
+dotnet_diagnostic.ide0110.severity = suggestion
+dotnet_diagnostic.ide0058.severity = suggestion
+dotnet_diagnostic.ide0059.severity = suggestion
+csharp_style_namespace_declarations = file_scoped:none
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
+csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
+csharp_style_prefer_extended_property_pattern = true:suggestion
+
+# Microsoft .NET properties
+dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.constants_rule.severity = none
+dotnet_naming_rule.constants_rule.style = begins_with_c_
+dotnet_naming_rule.constants_rule.symbols = constants_symbols
+dotnet_naming_rule.event_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.event_rule.severity = none
+dotnet_naming_rule.event_rule.style = pascal_case
+dotnet_naming_rule.event_rule.symbols = event_symbols
+dotnet_naming_rule.interfaces_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.interfaces_rule.severity = none
+dotnet_naming_rule.interfaces_rule.style = begins_with_i
+dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols
+dotnet_naming_rule.local_constants_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.local_constants_rule.severity = none
+dotnet_naming_rule.local_constants_rule.style = camelcase
+dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols
+dotnet_naming_rule.local_should_be_camelcase.import_to_resharper = as_predefined
+dotnet_naming_rule.method_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.method_rule.severity = none
+dotnet_naming_rule.method_rule.style = pascal_case
+dotnet_naming_rule.method_rule.symbols = method_symbols
+dotnet_naming_rule.non_field_members_should_be_pascal_case.import_to_resharper = True
+dotnet_naming_rule.non_field_members_should_be_pascal_case.resharper_description = non_field_members_should_be_pascal_case
+dotnet_naming_rule.non_field_members_should_be_pascal_case.resharper_guid = 44ab0b2d-34cc-42db-a691-0c646821e2e4
+dotnet_naming_rule.parameter_should_be_camelcase.import_to_resharper = as_predefined
+dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.private_constants_rule.severity = none
+dotnet_naming_rule.private_constants_rule.style = begins_with_c_
+dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
+dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.private_instance_fields_rule.severity = none
+dotnet_naming_rule.private_instance_fields_rule.style = starts_with_m_
+dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
+dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.private_static_fields_rule.severity = none
+dotnet_naming_rule.private_static_fields_rule.style = starts_with_s_
+dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
+dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.private_static_readonly_rule.severity = none
+dotnet_naming_rule.private_static_readonly_rule.style = starts_with_s_
+dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
+dotnet_naming_rule.property_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.property_rule.severity = none
+dotnet_naming_rule.property_rule.style = pascal_case
+dotnet_naming_rule.property_rule.symbols = property_symbols
+dotnet_naming_rule.public_fields_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.public_fields_rule.severity = none
+dotnet_naming_rule.public_fields_rule.style = starts_with_m_
+dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols
+dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.static_readonly_rule.severity = none
+dotnet_naming_rule.static_readonly_rule.style = starts_with_s_
+dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols
+dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper = as_predefined
+dotnet_naming_rule.types_and_namespaces_rule.severity = none
+dotnet_naming_rule.types_and_namespaces_rule.style = pascal_case
+dotnet_naming_rule.types_and_namespaces_rule.symbols = types_and_namespaces_symbols
+dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.constants_symbols.applicable_kinds = field
+dotnet_naming_symbols.constants_symbols.required_modifiers = const
+dotnet_naming_symbols.event_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.event_symbols.applicable_kinds = event
+dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface
+dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local
+dotnet_naming_symbols.local_constants_symbols.required_modifiers = const
+dotnet_naming_symbols.method_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.method_symbols.applicable_kinds = method
+dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
+dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
+dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
+dotnet_naming_symbols.property_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.property_symbols.applicable_kinds = property
+dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field
+dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field
+dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static,readonly
+dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace,class,struct,enum,delegate
+
+# ReSharper inspection severities
+resharper_arrange_accessor_owner_body_highlighting = suggestion
+resharper_arrange_default_value_when_type_evident_highlighting = hint
+resharper_arrange_object_creation_when_type_evident_highlighting = hint
+resharper_arrange_redundant_parentheses_highlighting = hint
+resharper_arrange_this_qualifier_highlighting = hint
+resharper_arrange_type_member_modifiers_highlighting = hint
+resharper_arrange_type_modifiers_highlighting = hint
+resharper_built_in_type_reference_style_for_member_access_highlighting = hint
+resharper_built_in_type_reference_style_highlighting = hint
+resharper_comment_typo_highlighting = hint
+resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting = none
+resharper_identifier_typo_highlighting = hint
+resharper_inconsistent_naming_highlighting = none
+resharper_json_validation_failed_highlighting = warning
+resharper_member_can_be_private_global_highlighting = hint
+resharper_member_can_be_private_local_highlighting = hint
+resharper_member_can_be_protected_global_highlighting = hint
+resharper_member_can_be_protected_local_highlighting = hint
+resharper_redundant_base_qualifier_highlighting = warning
+resharper_remove_redundant_braces_highlighting = suggestion
+resharper_string_literal_typo_highlighting = hint
+resharper_suggest_var_or_type_built_in_types_highlighting = hint
+resharper_suggest_var_or_type_elsewhere_highlighting = hint
+resharper_suggest_var_or_type_simple_types_highlighting = hint
+resharper_unused_auto_property_accessor_global_highlighting = hint
+resharper_unused_auto_property_accessor_local_highlighting = hint
+resharper_unused_member_global_highlighting = hint
+csharp_style_prefer_method_group_conversion = true:suggestion
+csharp_style_prefer_top_level_statements = true:silent
+dotnet_diagnostic.VSTHRD001.severity = none
+dotnet_diagnostic.VSTHRD105.severity = silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_prefer_system_threading_lock = true:suggestion
+dotnet_diagnostic.VSTHRD012.severity = none
+dotnet_diagnostic.VSTHRD003.severity = none
+
+[*.{cs,vb}]
+dotnet_diagnostic.ca1822.severity = silent
+dotnet_diagnostic.ca1305.severity = suggestion
+dotnet_diagnostic.IDE0079.severity = silent
+dotnet_diagnostic.IDE0270.severity = silent
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_diagnostic.ca2215.severity = warning
+dotnet_diagnostic.ca1001.severity = suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+indent_size = 4
+end_of_line = crlf
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_style_readonly_field = true:suggestion
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+dotnet_style_allow_multiple_blank_lines_experimental = true:silent
+dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
+dotnet_code_quality_unused_parameters = all:suggestion
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+
+[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}]
+indent_style = space
+indent_size = 4
+tab_width = 4
+
+[*.{appxmanifest,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cshtml,csproj,cu,cuh,cxx,dbml,discomap,dtd,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,jsproj,lsproj,mpp,mq4,mq5,mqh,njsproj,nuspec,proj,props,proto,razor,resw,resx,StyleCop,targets,tasks,tpp,usf,ush,vbproj,xml,xsd}]
+indent_style = space
+indent_size = 4
+tab_width = 4
+
+[*.{asax,ascx,aspx,axaml,cs,css,js,jsx,master,paml,skin,ts,tsx,vb,xaml,xamlx,xoml}]
+indent_style = space
+indent_size = 4
+tab_width = 4
+dotnet_diagnostic.CA1816.severity = silent
+dotnet_diagnostic.HAA0501.severity = silent
+dotnet_diagnostic.HAA0502.severity = silent
+dotnet_style_prefer_collection_expression = true:suggestion
+dotnet_diagnostic.RS2008.severity = none
+
+[*.{json,resjson}]
+indent_style = space
+indent_size = 4
+tab_width = 4
diff --git a/.gitignore b/.gitignore
index e3a6880..2a07abd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
-.vs
-bin
-obj
+.vs
+bin
+obj
diff --git a/AGENTS.linux.md b/AGENTS.linux.md
index 59a55ea..686e35c 100644
--- a/AGENTS.linux.md
+++ b/AGENTS.linux.md
@@ -1,3 +1,3 @@
-# Linux-specific instructions
-
+# Linux-specific instructions
+
- After every iteration, run `dotnet jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index a5333a4..f946b36 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,30 +1,30 @@
-# Platform and documentation
-
-If this is a linux environment, read `AGENTS.linux.md`.
-If this is a windows environment, read `AGENTS.windows.md`.
-Follow the guidelines laid out in `CODESTYLE.md`.
-Also see the other related technical documentation in the docs folder.
-
-## Rules
-
-- Prefer extracting code to a shared helper to be reused instead of duplicating code. Always keep high maintainability standards.
-- If a class is to be used only once, consider nesting it inside of another class. Otherwise place each newly created class into its own file. The file name must match the class name.
-- When asked to begin working on a task, create a detailed implementation plan first, present the plan to the user, and ask for approval before beginning with the actual implementation.
-- Don't make assumptions in the plan. If necessary, ask all clarifying questions before presenting the final plan.
-- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
-- If there's documnentation present, always keep it updated.
-- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
-
-### Git
-
-- Never change the .gitignore file without consent.
-- Keep changes small with minimal churn and commit often. If one iteration encompasses many smaller tasks with more than one commit, create a git branch and do the commits there. Let me review the branch before merging it back to master.
-- When multiple commits are necessary, pause after every commit and ask the user to give a command to proceed.
-- After every iteration, do a git commit with a brief summary of the changes as a commit message.
-- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
-- Never use `git restore`, `git checkout --`, reset commands, or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback.
-
-### Dotnet CLI
-
-- If you need a separate output directory, use a subfolder under `artifacts`, and clean it up afterwards.
-- Avoid running `dotnet build` and `dotnet test` in parallel in this repo; that can cause file-lock failures in `obj\Debug\net10.0`.
+# Platform and documentation
+
+If this is a linux environment, read `AGENTS.linux.md`.
+If this is a windows environment, read `AGENTS.windows.md`.
+Follow the guidelines laid out in `CODESTYLE.md`.
+Also see the other related technical documentation in the docs folder.
+
+## Rules
+
+- Prefer extracting code to a shared helper to be reused instead of duplicating code. Always keep high maintainability standards.
+- If a class is to be used only once, consider nesting it inside of another class. Otherwise place each newly created class into its own file. The file name must match the class name.
+- When asked to begin working on a task, create a detailed implementation plan first, present the plan to the user, and ask for approval before beginning with the actual implementation.
+- Don't make assumptions in the plan. If necessary, ask all clarifying questions before presenting the final plan.
+- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
+- If there's documnentation present, always keep it updated.
+- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
+
+### Git
+
+- Never change the .gitignore file without consent.
+- Keep changes small with minimal churn and commit often. If one iteration encompasses many smaller tasks with more than one commit, create a git branch and do the commits there. Let me review the branch before merging it back to master.
+- When multiple commits are necessary, pause after every commit and ask the user to give a command to proceed.
+- After every iteration, do a git commit with a brief summary of the changes as a commit message.
+- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
+- Never use `git restore`, `git checkout --`, reset commands, or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback.
+
+### Dotnet CLI
+
+- If you need a separate output directory, use a subfolder under `artifacts`, and clean it up afterwards.
+- Avoid running `dotnet build` and `dotnet test` in parallel in this repo; that can cause file-lock failures in `obj\Debug\net10.0`.
diff --git a/AGENTS.windows.md b/AGENTS.windows.md
index 4b43674..f552514 100644
--- a/AGENTS.windows.md
+++ b/AGENTS.windows.md
@@ -1,4 +1,4 @@
-# Windows-specific instructions
-
-- After the implementation is finished, run `python D:\Code\crlf.py $file1 $file2 ...` for changed files you recognize, in order to normalize all line endings of all touched files to CRLF.
+# Windows-specific instructions
+
+- After the implementation is finished, run `python D:\Code\crlf.py $file1 $file2 ...` for changed files you recognize, in order to normalize all line endings of all touched files to CRLF.
- After every iteration, run `jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
\ No newline at end of file
diff --git a/CODESTYLE.md b/CODESTYLE.md
index 3bc7baf..5c96a59 100644
--- a/CODESTYLE.md
+++ b/CODESTYLE.md
@@ -1,51 +1,51 @@
-# Code Style
-
-This repository follows the local `.editorconfig` and the style visible in the current local changes. Use these notes when creating new code.
-
-## Naming
-
-- Use PascalCase for namespaces, types, methods, properties, enum members, and non-field members.
-- Prefix enum type names with `E`, for example `ECellKind`, `EPipeMedium`, `EFailureKind`, and `EEditorTool`.
-- Prefix struct type names with `S` when creating new structs.
-- Prefix interfaces with `I`.
-- Use camelCase for parameters and local variables.
-- Prefix private instance fields with `m_` and keep the remainder PascalCase, for example `m_Level` and `m_SelectedTool`.
-- Prefix private static fields and static readonly fields with `s_`.
-- Prefix constants with `c_`.
-- Avoid `this.` unless it is needed for clarity or disambiguation.
-- Always use folder-based namespaces when creating types and refactoring.
-
-## Files And Types
-
-- Use file-scoped namespaces.
-- Keep one reusable top-level class per file, with the file name matching the class name.
-- If a helper type is used only by one class, prefer nesting it inside that class.
-- Keep small, cohesive files and extract shared helpers instead of duplicating logic.
-
-## Braces And Blocks
-
-- Use braces for multi-line bodies.
-- If nesting a for-loop under another for-loop, always include curly braces in the parent for-loop.
-- Omit braces for simple single-line embedded statements when readability stays clear.
-- Nested control flow with multi-line bodies should use braces at every multi-line level.
-- Keep opening braces on the next line for types, methods, properties, accessors, and control blocks.
-- Compact object initializers, switch expressions, and `with` expressions may keep the opening brace on the same line when cleanup formats them that way.
-
-## Blank Lines
-
-- Use a blank line to separate members.
-- Use a blank line after control-flow transfer clauses such as `return`, `continue`, `break`, and `throw` when more code follows in the same scope.
-- Avoid extra blank lines inside short methods and between tightly related statements.
-- Keep at most one blank line in code and declarations.
-
-## Expressions And Formatting
-
-- Prefer `var` when the type is apparent or not useful to repeat; use explicit built-in types such as `int`, `bool`, and `string`.
-- Prefer target-typed `new()` when the type is evident.
-- Prefer object and collection initializers, including collection expressions such as `[".json"]`.
-- Prefer pattern matching for combined checks, for example `cell is { HasPipe: true, Pressure: > 7 }`.
-- Prefer switch expressions for simple value selection.
-- Prefer expression-bodied properties and accessors when they remain simple.
-- Keep simple object initializers and property patterns on one line when they are short and readable.
-- Keep long boolean expressions and interpolated status strings readable without introducing unnecessary blank lines.
-- Keep using directives outside namespaces.
+# Code Style
+
+This repository follows the local `.editorconfig` and the style visible in the current local changes. Use these notes when creating new code.
+
+## Naming
+
+- Use PascalCase for namespaces, types, methods, properties, enum members, and non-field members.
+- Prefix enum type names with `E`, for example `ECellKind`, `EPipeMedium`, `EFailureKind`, and `EEditorTool`.
+- Prefix struct type names with `S` when creating new structs.
+- Prefix interfaces with `I`.
+- Use camelCase for parameters and local variables.
+- Prefix private instance fields with `m_` and keep the remainder PascalCase, for example `m_Level` and `m_SelectedTool`.
+- Prefix private static fields and static readonly fields with `s_`.
+- Prefix constants with `c_`.
+- Avoid `this.` unless it is needed for clarity or disambiguation.
+- Always use folder-based namespaces when creating types and refactoring.
+
+## Files And Types
+
+- Use file-scoped namespaces.
+- Keep one reusable top-level class per file, with the file name matching the class name.
+- If a helper type is used only by one class, prefer nesting it inside that class.
+- Keep small, cohesive files and extract shared helpers instead of duplicating logic.
+
+## Braces And Blocks
+
+- Use braces for multi-line bodies.
+- If nesting a for-loop under another for-loop, always include curly braces in the parent for-loop.
+- Omit braces for simple single-line embedded statements when readability stays clear.
+- Nested control flow with multi-line bodies should use braces at every multi-line level.
+- Keep opening braces on the next line for types, methods, properties, accessors, and control blocks.
+- Compact object initializers, switch expressions, and `with` expressions may keep the opening brace on the same line when cleanup formats them that way.
+
+## Blank Lines
+
+- Use a blank line to separate members.
+- Use a blank line after control-flow transfer clauses such as `return`, `continue`, `break`, and `throw` when more code follows in the same scope.
+- Avoid extra blank lines inside short methods and between tightly related statements.
+- Keep at most one blank line in code and declarations.
+
+## Expressions And Formatting
+
+- Prefer `var` when the type is apparent or not useful to repeat; use explicit built-in types such as `int`, `bool`, and `string`.
+- Prefer target-typed `new()` when the type is evident.
+- Prefer object and collection initializers, including collection expressions such as `[".json"]`.
+- Prefer pattern matching for combined checks, for example `cell is { HasPipe: true, Pressure: > 7 }`.
+- Prefer switch expressions for simple value selection.
+- Prefer expression-bodied properties and accessors when they remain simple.
+- Keep simple object initializers and property patterns on one line when they are short and readable.
+- Keep long boolean expressions and interpolated status strings readable without introducing unnecessary blank lines.
+- Keep using directives outside namespaces.
diff --git a/README.md b/README.md
index 489d10b..6a69046 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,18 @@
-# Reactor Maintenance
-
-C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `design.md`.
-
-## Projects
-
-- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, versioned JSON serialization, and swappable difficulty balancing profiles.
-- `src/ReactorMaintenance.Win2D`: Win2D editor app for painting floor/wall terrain plus cell props, loading/saving levels, advancing simulation turns, and activating the reactor.
-- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior.
-
-## Commands
-
-```powershell
-dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
-dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
-dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
-```
-
+# Reactor Maintenance
+
+C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `design.md`.
+
+## Projects
+
+- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, versioned JSON serialization, and swappable difficulty balancing profiles.
+- `src/ReactorMaintenance.Win2D`: Win2D editor app for painting floor/wall terrain plus cell props, loading/saving levels, advancing simulation turns, and activating the reactor.
+- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior.
+
+## Commands
+
+```powershell
+dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
+dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
+dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
+```
+
diff --git a/ReactorMaintenance.slnx b/ReactorMaintenance.slnx
index 64f4c3f..8ecfa60 100644
--- a/ReactorMaintenance.slnx
+++ b/ReactorMaintenance.slnx
@@ -1,11 +1,11 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/design.md b/docs/design.md
index f8b6e6d..2840dad 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -1,1280 +1,1280 @@
-# Game Design: Reactor Maintenance
-
-## Core Concept
-
-The player controls a maintenance robot inside a collapsing industrial reactor facility.
-
-The game is a deterministic, turn-based systems puzzle about stabilizing interconnected machinery under pressure. The player does not fight enemies and does not react to hidden surprises. Instead, they study a visible machine, predict its failures, and choose which systems to save, reroute, isolate, or sacrifice.
-
-The facility is made of:
-
-- pipes
-- pressure systems
-- generators
-- locks and bulkhead doors
-- machines
-- diagnostics systems
-- reactor support systems
-
-Each turn has three phases:
-
-1. Player phase: the player spends a limited number of actions.
-2. Simulation phase: the facility updates all mechanical systems.
-3. Event phase: scheduled failures or generated hurdles advance.
-
-The main fantasy is:
-
-> "I understand this broken machine well enough to prevent the next disaster."
-
-The game should feel:
-
-- logical
-- tactical
-- deterministic
-- readable
-- systemic
-
-It should not feel:
-
-- hectic
-- random
-- action-heavy
-- dependent on hidden information
-
-## Goal
-
-The reactor starts offline.
-
-To win a level, the player must bring a required set of main systems online, then activate the reactor core.
-
-Example required systems:
-
-- cooling online
-- pressure stable
-- main power active
-
-When all required systems are ready, the game shows:
-
-> REACTOR READY
-
-The player can then spend an action at the reactor control terminal to activate the core and win.
-
-## Loss Conditions
-
-The player loses if a global failure state reaches zero or a hard safety limit is exceeded.
-
-Possible loss conditions:
-
-- core meltdown
-- pressure overflow
-- total power failure
-- facility stability collapse
-
-Important: failure should not be instant unless the player allows a clearly predicted critical event to happen. Most losses happen through slowly degrading global values, giving the player time to react.
-
-## Turn Structure
-
-### 1. Player Phase
-
-The player has a fixed number of actions per turn, usually 3.
-
-Possible actions:
-
-- move
-- repair pipe
-- remove pipe
-- rotate valve
-- lock or unlock bulkhead door
-- toggle breaker
-- connect cable
-- activate system
-- inspect diagnostic terminal
-
-### 2. Simulation Phase
-
-The facility updates all active systems.
-
-Examples:
-
-- pressure flows through connected pipes
-- leaks reduce cooling or pressure efficiency
-- generators produce power and consume fuel
-- machines consume power
-- cooling reduces heat
-- fire or overheating spreads through connected sectors
-- unstable pipes advance toward failure
-
-### 3. Event Phase
-
-Predicted events advance by one turn.
-
-Examples:
-
-- a leak gets worse
-- a short circuit disables a cable
-- an unstable pipe breaks
-- a door jams
-- a machine overheats
-- a pressure spike enters a sector
-
-Events are not hidden. The player sees upcoming failures before they happen.
-
-## Information Design
-
-All relevant information is visible and predictable.
-
-The player can see:
-
-- current pressure values
-- current power flow
-- current cooling flow
-- global stability values
-- unstable pipes
-- active leaks
-- locked and unlocked doors
-- scheduled upcoming failures
-- predicted sector consequences
-
-The game is not about asking:
-
-> "What is behind the corner?"
-
-It is about asking:
-
-> "What happens if I let this system run for two more turns?"
-
-## The Diagnostic Eye
-
-The "eye" theme is represented by industrial diagnostic systems.
-
-These can be:
-
-- mechanical observation lenses
-- wall-mounted diagnostic terminals
-- reactor monitoring cameras
-- pressure prediction displays
-
-The eye is not an enemy and not a stealth mechanic. It is the factory's analysis interface.
-
-Example diagnostic output:
-
-> PRESSURE FAILURE PREDICTED IN SECTOR C IN 2 TURNS
-
-## Core Gameplay Systems
-
-### Pipes
-
-Pipes transport pressure, coolant, or fuel.
-
-Pipes can be:
-
-- intact
-- leaking
-- unstable
-- broken
-- removed
-
-Player interactions:
-
-- repair a pipe
-- remove a pipe
-- route pressure through a different pipe network
-
-### Valves
-
-Valves control flow direction through pipe networks.
-
-Player interactions:
-
-- rotate valve
-- open valve
-- close valve
-
-Valves create tradeoffs. Rerouting pressure away from the reactor may stabilize the core but overload another sector.
-
-### Power
-
-Generators produce power. Machines consume power.
-
-Power flows through cables, breakers, and connected sectors.
-
-Player interactions:
-
-- toggle breaker
-- connect cable
-- activate generator
-- deactivate nonessential machine
-
-### Bulkhead Doors
-
-Bulkhead doors isolate sectors.
-
-Player interactions:
-
-- lock door
-- unlock door
-
-Locking a door can contain a disaster, but anything behind the door may become inaccessible.
-
-### Machines
-
-Machines provide level-critical functions.
-
-Examples:
-
-- cooling pump
-- pressure regulator
-- backup generator
-- reactor control terminal
-- diagnostic terminal
-
-Machines may need power, coolant, pressure, or a stable sector to function.
-
-## System Interaction Model
-
-The facility is modeled as a graph.
-
-- sectors are rooms or compact areas
-- pipes, cables, doors, and vents are edges between sectors
-- machines occupy sectors
-- hazards occupy sectors or edges
-- flow networks are computed from currently connected edges
-
-Each sector tracks a small set of readable values:
-
-| Value | Range | Meaning |
-| ----- | ----- | ------- |
-| Heat | 0-10 | Local temperature and fire risk |
-| Smoke | 0-10 | Visibility and movement penalty |
-| Fuel Vapor | 0-10 | Flammable gas concentration |
-| Coolant Pooling | 0-10 | Liquid coolant on the floor |
-| Electrical Charge | 0-10 | Exposed live current or arcing risk |
-| Stability | 0-10 | Structural and machinery integrity |
-
-Each pipe edge tracks:
-
-| Value | Range | Meaning |
-| ----- | ----- | ------- |
-| Medium | enum | pressure, coolant, or fuel |
-| Flow | 0-10 | Amount moving through the pipe this turn |
-| Pressure | 0-10 | Stress on the pipe |
-| Integrity | 0-10 | Damage before breaking |
-| Leak Rate | 0-10 | Amount lost into the sector each turn |
-
-The player never needs to inspect hidden formulas. The interface presents the result as direct predictions:
-
-- `FUEL VAPOR IGNITION IN 2 TURNS`
-- `COOLANT SHORT WILL DISABLE BREAKER NEXT TURN`
-- `SMOKE WILL COST +1 MOVE IN SECTOR D`
-- `PUMP CAVITATION: COOLING OUTPUT -2`
-
-### Flow Resolution
-
-During the simulation phase, each active network resolves in this order:
-
-1. Power networks energize machines, doors, breakers, and exposed cables.
-2. Fuel networks feed generators and leak into sectors.
-3. Pressure networks move stress through pipes and regulators.
-4. Coolant networks remove heat, pool through leaks, and feed pumps.
-5. Sector hazards update: heat, vapor, smoke, fire, charge, and stability.
-6. Machines update their output based on the final local conditions.
-
-This order makes cause and effect predictable. For example, a generator consumes fuel before it produces heat, then coolant removes some of that heat, then remaining heat may ignite vapor.
-
-### Fuel Leakage
-
-Fuel leaks are dangerous because fuel has two forms:
-
-- liquid fuel spilled in a sector
-- fuel vapor produced by heat, pressure spray, or accumulated spills
-
-Fuel leakage affects the facility in four ways:
-
-- lost fuel reduces generator runtime
-- fuel vapor creates ignition risk
-- liquid fuel slows movement
-- fire consumes fuel and spreads heat
-
-Simulation:
-
-- A leaking fuel pipe adds `Leak Rate` fuel to the sector each turn.
-- If sector heat is 4 or higher, some spilled fuel becomes vapor.
-- If pipe pressure is 7 or higher, leaked fuel sprays and becomes vapor faster.
-- Vapor decays slowly through ventilation or open doors.
-- Liquid fuel remains until drained, burned, sealed, or cleaned by a maintenance action.
-
-Player-solvable responses:
-
-- repair the fuel pipe
-- close an upstream fuel valve
-- reroute fuel through another pipe
-- depressurize the line before repairing it
-- ventilate the sector to reduce vapor
-- lock bulkheads to contain vapor
-- shut down ignition sources before entering or restarting machinery
-
-### Coolant Leakage
-
-Coolant leaks are less explosive than fuel leaks but can disable systems indirectly.
-
-Coolant leakage affects the facility in four ways:
-
-- less coolant reaches the reactor or overheated machines
-- pooled coolant conducts electricity
-- coolant vapor creates steam that reduces visibility
-- coolant contacting very hot machinery causes thermal shock
-
-Simulation:
-
-- A leaking coolant pipe subtracts from delivered coolant and adds `Coolant Pooling` to the local sector.
-- If coolant pooling touches exposed electrical charge, the sector becomes electrified.
-- If coolant pooling reaches heat 7 or higher, it becomes steam and increases smoke.
-- If a hot machine receives a sudden large coolant flow, its stability drops by 1 unless it is throttled through a regulator.
-
-Player-solvable responses:
-
-- repair or replace the coolant pipe
-- route coolant around the leak
-- cut power before walking through pooled coolant
-- open a drain before restarting cooling
-- throttle coolant flow into overheated machinery
-- use a bulkhead to isolate steam and preserve visibility elsewhere
-
-### Movement Implications
-
-Movement is not only a distance cost. The facility state changes how expensive or risky movement is.
-
-Sector movement modifiers:
-
-| Condition | Movement Effect | Extra Risk |
-| --------- | --------------- | ---------- |
-| Smoke 4+ | entering costs +1 move | diagnostic range reduced |
-| Smoke 8+ | entering costs +2 move | cannot inspect across sector |
-| Liquid Fuel 4+ | entering costs +1 move | ignition risk follows player tools |
-| Coolant Pooling 4+ | entering costs +1 move | electrocution if charged |
-| Active Fire | blocked unless shielded or suppressed | robot heat rises |
-| Electrified Floor | blocked unless power is cut or insulated | robot damage |
-| Low Pressure / Venting | entering costs +1 move | may push smoke or vapor |
-
-The robot is not fragile, but it should not ignore the environment. Hazards create routing puzzles:
-
-- the shortest path may become too slow
-- a safe path may require toggling breakers first
-- a locked door may protect the reactor but trap the robot away from tools
-- venting smoke may clear movement while spreading vapor into another sector
-
-### Conditions for Fire
-
-Fire starts only when fuel, oxygen, and ignition are all present.
-
-Fuel sources:
-
-- fuel vapor 4+
-- liquid fuel 6+
-- burning machine
-- overheated generator
-
-Oxygen sources:
-
-- normal unsealed sector
-- open vent
-- open bulkhead connection
-
-Ignition sources:
-
-- heat 8+
-- electrical arc
-- overloaded generator
-- reactor startup spark
-- pressure rupture through a fuel line
-
-Simulation:
-
-- If all three conditions are present, the sector gains `Fire`.
-- Fire adds heat each turn.
-- Fire converts nearby liquid fuel into vapor, then consumes it.
-- Fire damages machines and pipes in the sector.
-- Fire adds smoke to the sector and adjacent open sectors.
-- Fire cannot pass through a locked fire-rated bulkhead, but heat can slowly stress that door.
-- Fire dies when fuel is exhausted, oxygen is removed, or suppression exceeds fire intensity.
-
-Player-solvable responses:
-
-- remove fuel by closing valves or draining liquid
-- remove oxygen by sealing bulkheads or disabling vents
-- remove ignition by cutting power or cooling the sector
-- suppress fire using coolant mist, foam canisters, or vent purge systems
-- deliberately burn off fuel in an isolated sector when the reactor path must be cleared
-
-### Fuel and Coolant Interaction
-
-Fuel and coolant interact in useful and dangerous ways.
-
-- Coolant can reduce heat and prevent fuel ignition.
-- Coolant and fuel liquids don't mix.
-- Coolant mist and fuel vapor don't mix.
-- Coolant mist can suppress fire.
-- A coolant leak near a fuel leak can create a false sense of safety: ignition is delayed while cooling works, then becomes critical if coolant supply runs out.
-
-## Problematic States and Player-Solvable Hurdles
-
-A hurdle is a problematic state that blocks progress, threatens a loss condition, or changes the route the player can safely use. Every generated hurdle must have at least two viable responses unless the level is explicitly teaching a mechanic.
-
-### Fuel Leak Near Ignition Source
-
-Problem:
-
-A fuel pipe leaks into a sector containing an electrical arc, hot machine, or generator. Vapor will ignite after a visible countdown.
-
-System interactions:
-
-- fuel loss reduces generator runtime
-- vapor increases fire risk
-- liquid fuel slows movement
-- open doors or vents may spread vapor
-- coolant may delay ignition by lowering heat
-
-Viable solutions:
-
-- close the upstream fuel valve, then repair the pipe
-- cut power to the ignition source and vent the vapor
-- reroute fuel through a backup line
-- lock both bulkheads, allow controlled burnoff, then reopen after fire dies
-- activate coolant mist to keep heat below ignition while completing another task
-
-Simulation details:
-
-- Each turn, the pipe adds fuel equal to `Leak Rate`.
-- Heat converts some liquid fuel into vapor.
-- Vapor follows open airflow edges during hazard update.
-- Ignition countdown is calculated from vapor, heat, and active ignition sources.
-- Repairing under pressure costs an extra action or risks increasing leak rate by 1.
-
-### Coolant Leak Across Powered Cable
-
-Problem:
-
-A coolant leak is pooling across a live cable or breaker. The sector will become electrified, blocking movement and possibly disabling power.
-
-System interactions:
-
-- coolant delivery to the reactor is reduced
-- exposed power converts pooling into electrical charge
-- electrified coolant blocks or damages the robot
-- cutting power may shut down required machines
-
-Viable solutions:
-
-- toggle the breaker off, cross safely, repair the pipe, then restore power
-- reroute power around the wet sector
-- drain coolant before restoring the cable
-- reroute coolant through a dry pipe path
-- lock the sector and use a remote terminal to operate the machine beyond it
-
-Simulation details:
-
-- Pooling increases by leak rate each turn.
-- If a live cable exists and pooling is 3+, electrical charge rises.
-- At charge 4+, movement through the sector is blocked unless insulated.
-- At charge 7+, the breaker trips and the connected power network splits.
-- Drains reduce pooling at a fixed rate but may move coolant into lower sectors.
-
-### Fuel Leak Plus Coolant Leak
-
-Problem:
-
-Fuel and coolant leak into the same sector. The coolant suppresses heat temporarily, but the combined spill slows movement and can spread vapor through steam or drainage.
-
-System interactions:
-
-- coolant delays ignition while available
-- fuel vapor rises if heat climbs after coolant flow stops
-- drainage can spread liquid fuel to another sector
-- steam reduces visibility and increases movement cost
-- repairing either pipe changes the balance of the hazard
-
-Viable solutions:
-
-- repair fuel first if ignition risk is rising
-- repair coolant first if reactor heat is becoming critical
-- open drains only after isolating downstream sectors
-- vent steam after closing fuel valves
-- maintain coolant flow deliberately while rerouting fuel elsewhere
-
-Simulation details:
-
-- Fuel and coolant pooling are tracked separately.
-- Coolant reduces heat before vapor is produced.
-- If heat remains 7+ after cooling, steam rises and carries a fraction of vapor along vent paths.
-- Drainage moves a portion of both liquids unless a separator machine is powered.
-- The diagnostic display shows which repair has the larger predicted effect over the next two turns.
-
-### Fire Blocking the Critical Route
-
-Problem:
-
-An active fire blocks the shortest path to a required machine or terminal.
-
-System interactions:
-
-- fire raises heat and smoke
-- smoke increases movement cost and reduces diagnostic range
-- fire damages nearby pipes, possibly creating secondary leaks
-- bulkheads can contain fire but may cut off access
-- coolant can suppress fire but may cause electrical hazards
-
-Viable solutions:
-
-- seal bulkheads and take a longer route
-- cut power, then use coolant mist for suppression
-- vent smoke away from the route after removing fuel source
-- sacrifice the burning sector while powering the required machine remotely
-- drain fuel from the sector, wait for fire to consume remaining vapor, then pass
-
-Simulation details:
-
-- Fire intensity starts at 1-3 based on ignition strength.
-- Each turn, fire consumes fuel and adds heat and smoke.
-- If fuel remains and oxygen is present, intensity rises.
-- Suppression reduces intensity before spread is calculated.
-- Fire spreads only through open doors, vents, or flammable pipe ruptures.
-
-### Smoke-Filled Sector
-
-Problem:
-
-Smoke does not immediately destroy anything, but it makes movement slower and prediction weaker. The player may lose action efficiency before reaching a timed failure.
-
-System interactions:
-
-- fire creates smoke
-- steam from coolant leaks also counts as smoke
-- vents can clear smoke or move it
-- diagnostics become less useful through heavy smoke
-- smoke can hide floor hazards only if the player has not scanned the sector
-
-Viable solutions:
-
-- activate ventilation toward a safe exhaust path
-- open a bulkhead to disperse smoke, accepting spread risk
-- route around the sector
-- clear the originating fire or steam source
-- use a diagnostic terminal to inspect through smoke before entering
-
-Simulation details:
-
-- Smoke rises in sectors with fire or steam.
-- Ventilation moves smoke along active vent edges.
-- Closed bulkheads block smoke spread.
-- Smoke 4+ adds movement cost.
-- Smoke 8+ prevents long-range inspection through that sector.
-
-### Pressure Rupture in Fuel Line
-
-Problem:
-
-A pressurized fuel line is overloaded. If it ruptures, it will spray vapor instead of leaking liquid, creating a fast ignition threat.
-
-System interactions:
-
-- pressure stress damages the pipe
-- fuel vapor accumulates faster under pressure
-- venting pressure may push vapor into other sectors
-- shutting fuel may starve the generator
-- a pressure regulator can save the pipe but may overload a neighboring route
-
-Viable solutions:
-
-- rotate valves to reduce pressure before the rupture turn
-- vent pressure into a noncritical empty sector
-- shut down the generator fed by the line and close the fuel valve
-- reinforce the pipe to buy time, then reroute
-- isolate adjacent ignition sources before allowing a controlled rupture
-
-Simulation details:
-
-- Pipe integrity drops when pressure exceeds rated capacity.
-- At integrity 0, the pipe becomes broken and leak mode changes to spray.
-- Spray converts most leaked fuel directly into vapor.
-- Pressure venting reduces pipe stress but moves vapor along the selected vent path.
-- Reinforcement increases integrity but does not reduce pressure.
-
-### Generator Starved by Fuel Loss
-
-Problem:
-
-A required generator is online but its fuel supply is leaking. It will shut down before the player can activate the reactor.
-
-System interactions:
-
-- power output depends on fuel flow
-- low fuel causes brownouts, which can jam doors or stop pumps
-- repairing the leak may require moving through the leaking sector
-- routing more fuel may increase pressure and leak rate
-
-Viable solutions:
-
-- repair the fuel pipe
-- reduce power load by disabling nonessential machines
-- start a backup generator
-- reroute fuel through a lower-pressure path
-- store enough capacitor charge to bridge a short outage
-
-Simulation details:
-
-- Generator fuel buffer decreases when incoming fuel is below demand.
-- At low buffer, output becomes unstable and power flickers.
-- Flicker advances jam timers on powered doors and reduces pump efficiency.
-- Load shedding reduces fuel demand immediately.
-- Backup generators may produce less power but require fewer stable inputs.
-
-### Overheated Generator with Nearby Fuel
-
-Problem:
-
-A generator is producing needed power but heating a sector that contains fuel vapor or a leaking fuel pipe.
-
-System interactions:
-
-- power stability conflicts with fire safety
-- coolant can reduce heat but may short exposed cables
-- shutting the generator may disable the coolant pump
-- ventilation may cool the room but add oxygen
-
-Viable solutions:
-
-- throttle generator output and shed load
-- route coolant through a regulator before cooling the generator
-- repair the fuel leak before raising generator output
-- power the coolant pump from a backup source, then shut down the generator
-- seal the sector and run the generator only long enough to charge capacitors
-
-Simulation details:
-
-- Generator heat output scales with load.
-- If heat reaches 8 while vapor is 4+, ignition is predicted.
-- Coolant reduces heat after generator output, so undersized cooling may only delay ignition.
-- Load shedding lowers both heat and fuel consumption.
-- Capacitors store temporary power but discharge predictably each turn.
-
-### Coolant Starvation and Reactor Heat Rise
-
-Problem:
-
-The reactor needs cooling, but coolant is leaking, blocked, or diverted. Reactor heat rises toward meltdown.
-
-System interactions:
-
-- coolant leaks reduce delivered cooling
-- pressure can improve flow but stress pipes
-- power loss stops pumps
-- coolant sent to one overheated machine may starve the reactor
-- steam can block access to valves
-
-Viable solutions:
-
-- repair the leak on the main coolant route
-- reroute coolant through backup pipes
-- increase pump pressure briefly, accepting pipe stress elsewhere
-- throttle or shut down noncritical hot machines
-- use emergency coolant reserve for a short delay
-
-Simulation details:
-
-- Reactor heat increases by core instability minus delivered cooling.
-- Delivered cooling equals pump output minus leaks and blocked valves.
-- Pump output requires power and may require pressure.
-- Overpressure increases flow this turn but damages weak pipes.
-- Emergency reserve reduces heat immediately but has limited charges.
-
-### Electrified Bulkhead
-
-Problem:
-
-A bulkhead is needed for containment, but its controls are live, wet, or shorted. The player cannot safely lock or unlock it from the local panel.
-
-System interactions:
-
-- coolant or water pooling conducts charge
-- power loss may freeze the door in its current state
-- open door spreads smoke, vapor, fire, or pressure
-- closed door may trap the player away from required tools
-
-Viable solutions:
-
-- cut power to the door circuit, operate manual override, then restore power
-- drain the sector before touching the controls
-- reroute control power through another breaker
-- use a remote terminal if diagnostics are online
-- isolate hazards elsewhere so the door no longer needs to move
-
-Simulation details:
-
-- Door controls require power unless manual override is available.
-- If control panel charge is 4+, local operation is blocked.
-- If charge reaches 8, the door jams and requires repair.
-- Remote operation requires diagnostic network connectivity.
-- Manual override costs extra actions but ignores power state.
-
-### Jammed Door During Hazard Spread
-
-Problem:
-
-A door that should contain a hazard is jammed open or closed.
-
-System interactions:
-
-- jammed open allows smoke, vapor, pressure, or fire to spread
-- jammed closed can block the only path to a required system
-- low power or brownouts make jams worse
-- pressure difference across the door may prevent manual movement
-
-Viable solutions:
-
-- stabilize power, then operate the door
-- equalize pressure on both sides
-- repair the door actuator
-- reroute around the sector
-- create a different containment boundary using adjacent doors
-
-Simulation details:
-
-- Jam state has a severity from 1-3.
-- Each repair action reduces severity by 1 if pressure difference is safe.
-- Brownouts increase severity on powered doors with active faults.
-- High pressure difference blocks manual override.
-- A closed containment loop is valid even if it uses different doors than the obvious one.
-
-### Pressure Venting Through Hazardous Sector
-
-Problem:
-
-The safe pressure relief path crosses a sector with fuel vapor, fire, or coolant steam. Venting may solve pressure while spreading the hazard.
-
-System interactions:
-
-- pressure flow moves smoke and vapor
-- venting can add oxygen to a fire
-- pressure can push coolant pooling into lower sectors
-- not venting may burst a pipe
-
-Viable solutions:
-
-- clear the hazardous sector before venting
-- vent into a sealed sacrifice sector
-- rotate valves to use a lower-capacity but cleaner path
-- lock bulkheads downstream before opening the vent
-- reinforce the pressure pipe and delay venting until another route is ready
-
-Simulation details:
-
-- Venting reduces network pressure by a fixed amount.
-- Any vapor or smoke in the vent path is moved one sector per turn.
-- If venting feeds a fire with oxygen, fire intensity rises.
-- Sealed sacrifice sectors absorb pressure but lose stability.
-- Reinforced pipes can survive overpressure for a limited number of turns.
-
-### Unstable Pipe Chain Reaction
-
-Problem:
-
-Several damaged pipes are connected. Fixing only the first visible leak may increase flow through the next weak pipe and cause a later burst.
-
-System interactions:
-
-- repaired pipes restore flow and raise downstream pressure
-- valves can protect one branch by stressing another
-- coolant, fuel, and pressure networks may share physical sectors
-- burst pipes can create fire, steam, or movement hazards depending on medium
-
-Viable solutions:
-
-- repair the weakest downstream pipe before restoring full flow
-- throttle flow with a regulator
-- reroute around the damaged chain
-- isolate the branch and accept reduced system output
-- use temporary reinforcement to survive until reactor activation
-
-Simulation details:
-
-- Each pipe has rated pressure and integrity.
-- Flow restoration recalculates pressure across the whole network.
-- Diagnostics show the next predicted failure after a planned valve state.
-- Regulators cap flow but reduce delivered output.
-- Temporary reinforcement decays after a fixed number of turns.
-
-### Diagnostic Blind Spot
-
-Problem:
-
-A required prediction path is obscured by smoke, power loss, or a damaged diagnostic terminal. The player still knows the rules, but future consequences are less projected.
-
-System interactions:
-
-- smoke limits line-of-sight diagnostics
-- power loss disables terminals and cameras
-- fire or coolant shorts can damage sensors
-- blind spots make multi-turn planning harder but should not hide immediate lethal threats
-
-Viable solutions:
-
-- restore power to diagnostics
-- clear smoke or reroute camera feeds
-- repair the local sensor terminal
-- physically inspect the sector
-- use conservative containment: close valves, lock doors, and stabilize before proceeding
-
-Simulation details:
-
-- Blind sectors still show current visible hazards when entered.
-- Predictions beyond the blind sector are shortened or unavailable.
-- Repairing diagnostics restores full forecast for connected systems.
-- The generator must not place a mandatory invisible-only solution behind a blind spot.
-
-### Reactor Startup Hazard
-
-Problem:
-
-The reactor can be activated, but startup creates a predictable heat, pressure, and ignition pulse. Existing leaks can make the final activation unsafe.
-
-System interactions:
-
-- startup increases heat and pressure for one or more turns
-- fuel vapor in the reactor sector may ignite
-- weak coolant pipes may burst under final pump load
-- power demand may brown out doors or diagnostics
-
-Viable solutions:
-
-- pre-cool the reactor before activation
-- vent pressure immediately before startup
-- clear or seal fuel vapor in adjacent sectors
-- charge capacitors to cover startup power demand
-- lock containment doors so a noncritical sector absorbs the pulse
-
-Simulation details:
-
-- Startup applies a scripted but visible pulse profile.
-- The forecast panel shows startup consequences before the player commits.
-- If required systems are online but unstable, `REACTOR READY` becomes `REACTOR READY - UNSAFE STARTUP`.
-- Unsafe startup is allowed if it does not violate hard loss limits, preserving player agency.
-
-## Mechanic Simulation Reference
-
-The simulation should be simple enough to predict by inspection.
-
-### Heat
-
-- Heat rises from reactor instability, fire, overloaded generators, and hot machines.
-- Heat falls from coolant, ventilation, shutdown machinery, and time in stable sectors.
-- Heat 7+ accelerates fuel vapor creation.
-- Heat 8+ counts as an ignition source.
-- Heat 10 damages machines and pipes each turn.
-
-### Fuel
-
-- Fuel is consumed by generators.
-- Fuel can leak as liquid or spray as vapor.
-- Liquid fuel slows movement at high pooling values.
-- Vapor is the main fire risk.
-- Ventilation reduces vapor locally but may move it elsewhere.
-
-### Coolant
-
-- Coolant removes heat when delivered to a machine or sector.
-- Coolant leaks reduce delivered cooling and create pooling.
-- Pooling can conduct electricity.
-- Coolant contacting high heat creates steam.
-- Sudden coolant on overheated machinery can reduce stability unless regulated.
-
-### Power
-
-- Power is generated, routed, consumed, stored, and lost.
-- Brownouts do not immediately end the level; they reduce output, jam doors, and weaken pumps.
-- Exposed live power plus coolant pooling creates electrical charge.
-- Breakers define power network boundaries and are important player controls.
-
-### Pressure
-
-- Pressure moves useful flow but damages weak pipes.
-- Overpressure creates predicted bursts.
-- Pressure vents can save pipes but move airborne hazards.
-- Pressure differences affect door operation.
-- Regulators trade output for safety.
-
-### Fire and Smoke
-
-- Fire requires fuel, oxygen, and ignition.
-- Fire produces heat and smoke.
-- Smoke slows movement and shortens diagnostics.
-- Fire spreads through open routes and burns fuel.
-- Suppression, oxygen removal, or fuel removal can end fire.
-
-### Stability
-
-- Stability represents how close a sector, machine, or pipe is to mechanical failure.
-- Stability drops from heat, fire, pressure bursts, electrical shorts, and thermal shock.
-- Repairs restore local stability, not global safety by magic.
-- Global facility stability drops when critical sectors fail, when fire spreads, or when multiple machines break.
-
-## Hurdle Design Rules
-
-Every hurdle should be generated with:
-
-- a clear affected system
-- a visible countdown or consequence
-- at least one local solution
-- at least one systemic solution
-- one tradeoff that may create a secondary problem
-- a validation check that the player can physically reach or remotely affect the solution
-
-Local solutions are direct actions:
-
-- repair the pipe
-- toggle the breaker
-- rotate the valve
-- lock the door
-- drain the floor
-
-Systemic solutions change the wider machine:
-
-- reroute flow
-- shed load
-- sacrifice a sector
-- delay activation
-- isolate a hazard and let it burn out
-- use backup storage or reserves
-
-The generator should reject a hurdle if:
-
-- the only solution requires a disabled mechanic
-- a hard loss occurs before the player can take the minimum required actions
-- the solution requires crossing a blocked sector without an alternate route
-- the forecast cannot explain why the problem is happening
-- two hurdles combine into an unsolvable state before the player can respond
-
-## Procedural Level Generation
-
-Levels are generated deterministically from a compact generation request.
-
-
-## Generation Inputs
-
-### Seed
-
-The seed initializes the deterministic random number generator.
-
-### Enabled Mechanics
-
-The enabled mechanics list defines which systems may appear in the level.
-
-Example mechanics:
-
-- `power`
-- `cooling`
-- `doors`
-- `fuel`
-- `failures`
-
-The generator may only use mechanics from this list.
-
-If a mechanic is not enabled:
-
-- it must not be required for victory
-- it must not appear as an unresolved hurdle
-- it must not be needed to understand another generated system
-
-For every mechanic enabled:
-
-- the generator should include at least one meaningful use of that mechanic
-- the mechanic may appear as a direct hurdle, a solution path, or an interaction with another hurdle
-- if the mechanic is required for victory, the level must teach or expose its state before it becomes critical
-
-Mechanic requirements:
-
-| Mechanic | Required Presence | Example Hurdle | Example Solution Role |
-| -------- | ----------------- | -------------- | --------------------- |
-| `power` | at least one machine needs routed power | split power network | shed load to keep coolant pump online |
-| `cooling` | at least one heat source needs cooling | coolant starvation | suppress fire or protect generator |
-| `doors` | at least one sector boundary matters | jammed bulkhead | isolate vapor, fire, smoke, or pressure |
-| `fuel` | at least one generator or fuel pipe matters | fuel leak near ignition | choose between power uptime and fire safety |
-| `pressure` | at least one pipe network carries stress | pressure rupture | improve flow or vent hazard |
-| `failures` | at least one timed degradation occurs | pipe burst in 3 turns | repair, reroute, isolate, or delay |
-
-For every disabled mechanic, the level cannot generate associated tiles nor require associated actions.
-
-Disabled mechanics also limit interactions. For example:
-
-- if `fuel` is disabled, fire cannot require fuel-vapor reasoning
-- if `cooling` is disabled, overheated machines must be solvable through shutdown, venting, or power load changes
-- if `doors` are disabled, containment cannot require bulkhead locking
-- if `power` is disabled, machines may be manually operated, pressure-driven, or already powered by abstract local supply
-
-### Interaction Density
-
-The generator should distinguish primary hurdles from interaction consequences.
-
-Primary hurdle:
-
-- the problem counted by `Hurdle Count`
-- visible from the start or revealed by an explicit scheduled event
-- has a named forecast and at least two viable responses
-
-Interaction consequence:
-
-- emerges from simulation because of a primary hurdle or player solution
-- does not count against `Hurdle Count` unless it is deliberately generated as a primary hurdle
-- must be forecast before it becomes critical
-
-Example:
-
-- Primary hurdle: fuel pipe leak near Generator A.
-- Interaction consequence: coolant mist can suppress the heat but may electrify the wet floor if Breaker B remains on.
-
-The generator can deliberately pair mechanics:
-
-| Pair | Readable Interaction |
-| ---- | -------------------- |
-| fuel + power | generator uptime versus ignition risk |
-| fuel + cooling | cooling delays ignition but leaks may create steam |
-| cooling + power | coolant pooling creates electrical hazards |
-| pressure + fuel | rupture converts liquid leak into vapor spray |
-| pressure + doors | pressure difference blocks manual door operation |
-| fire + doors | containment protects the reactor but may block access |
-| smoke + diagnostics | predictions shorten until visibility is restored |
-
-Difficulty can increase interaction density, but it should not make every hurdle interact with every other hurdle. A readable level usually has one dominant interaction and one secondary interaction.
-
-### Hurdle Count
-
-Defines how many major problems the level contains.
-
-A hurdle is a meaningful obstacle that requires player attention, not just decorative damage.
-
-Examples:
-
-- a leaking pipe that prevents cooling from reaching the reactor
-- a pressure route that will burst in 3 turns unless rerouted
-- a locked door separating the player from a required generator
-- a short circuit splitting the power network
-- an overheating machine that must be cooled before activation
-- a sector that must be sacrificed to protect the core
-
-The generator should create an exact amount of primary hurdles.
-
-Secondary consequences may emerge from simulation, but the initial readable puzzle should have the requested number of main hurdles.
-
-### Difficulty
-
-Difficulty controls pressure on the player, not randomness.
-
-It can affect:
-
-- max move distance
-- starting global resource values
-- distance between important machines
-- urgency of scheduled failures
-- amount of spare power, coolant, or fuel
-- number of safe rerouting options
-- repair costs
-- density of damaged infrastructure
-- how many hurdles interact with each other
-
-Difficulty should not make the level less readable. Higher difficulty should create tighter tradeoffs and less slack.
-
-### Solvability Validation
-
-After generation, the level should run a deterministic validation pass.
-
-Validation checks:
-
-- every required system has a reachable activation path
-- every primary hurdle has at least two possible solution plans
-- no hard loss occurs before the player can take the shortest valid response
-- every forecasted hazard has at least one visible cause
-- generated interactions use only enabled mechanics
-- movement hazards do not trap the player without a remote solution or alternate route
-- final reactor startup has at least one safe or knowingly risky path
-
-The validator does not need to find the best solution. It only needs to prove that the level is not impossible, unreadable, or dependent on hidden information.
-
-## Hurdle Template Examples
-
-### Cooling Pipe Leak
-
-Problem:
-
-Cooling does not reach the reactor. Reactor heat rises every turn.
-
-Solutions:
-
-- repair the leaking pipe
-- reroute cooling through an alternate pipe path
-- activate backup cooling
-
-Difficulty modifiers:
-
-- higher difficulty places the leak farther from the player
-- higher difficulty adds a scheduled pipe burst
-- higher difficulty reduces starting cooling
-
-### Pressure Overload
-
-Problem:
-
-Pressure is building in a pipe network and will burst in a visible number of turns.
-
-Solutions:
-
-- rotate valve to redirect pressure
-- vent pressure into a noncritical sector
-- isolate the overloaded sector
-
-Difficulty modifiers:
-
-- higher difficulty creates consequences for every reroute
-- higher difficulty gives fewer safe vent paths
-
-### Split Power Network
-
-Problem:
-
-A required machine is not powered because the power network is split.
-
-Solutions:
-
-- reconnect cable
-- toggle breaker
-- activate backup generator
-- deactivate a nonessential machine to free capacity
-
-Difficulty modifiers:
-
-- higher difficulty reduces spare power
-- higher difficulty makes the player choose which machine stays offline
-
-### Jammed Bulkhead
-
-Problem:
-
-A sector must be isolated, but a door is jammed or unpowered.
-
-Solutions:
-
-- repair door control
-- restore power to the door
-- take a longer route to another door
-
-Difficulty modifiers:
-
-- higher difficulty adds a countdown before the hazard spreads
-- higher difficulty places useful resources behind the same door
-
-### Overheating Machine
-
-Problem:
-
-A required machine cannot be activated until heat is reduced.
-
-Solutions:
-
-- route cooling to the sector
-- reduce power load
-- activate venting system
-
-Difficulty modifiers:
-
-- higher difficulty links cooling to another already stressed system
-
-## Example Level: Cooling Sector B
-
-### Starting Global Values
-
-| System | Value |
-| ------ | ----- |
-| Power | 6/10 |
-| Cooling | 2/10 |
-| Pressure | 7/10 |
-| Heat | 6/10 |
-| Stability | 5/10 |
-
-### Hurdles
-
-1. Cooling pipe leak between the pump room and reactor chamber.
-2. Pressure overload in the maintenance shaft, predicted to burst in 3 turns.
-3. Bulkhead door can isolate the damaged sector, but doing so cuts off access to a generator.
-
-### Player Options
-
-#### Option A: Repair the Cooling Pipe
-
-Cost:
-
-- 2 actions
-
-Effect:
-
-- cooling reaches the reactor again
-
-Tradeoff:
-
-- the pressure overload remains unresolved
-
-#### Option B: Rotate the Pressure Valve
-
-Cost:
-
-- 1 action
-
-Effect:
-
-- reactor pressure stabilizes
-
-Tradeoff:
-
-- pressure rises in the maintenance shaft
-- pipe burst predicted there in 3 turns
-
-#### Option C: Lock the Bulkhead Door
-
-Cost:
-
-- 1 action
-
-Effect:
-
-- damaged sector is isolated
-- escalation stops there
-
-Tradeoff:
-
-- generator behind the door becomes unreachable
-- global power will slowly decline
-
-## Design Pillars
-
-### Deterministic Systems
-
-The same actions in the same state always produce the same outcome.
-
-### Visible Consequences
-
-The player should see upcoming failures and understand why they will happen.
-
-### Meaningful Tradeoffs
-
-The player should often solve one problem by making another problem worse.
-
-### Procedural but Authored-Feeling
-
-Generated levels should feel intentionally designed. The generator should use templates, validation, and readable system graphs instead of placing random damage everywhere.
-
-### Resource Prioritization
-
-The real puzzle is not a single lock-and-key solution. It is deciding which systems matter most and which losses are acceptable.
-
-## Summary
-
-This is an industrial chain-reaction puzzle game.
-
-The player wins by understanding a deterministic machine under stress. Procedural generation creates replayable levels from a seed, a list of enabled mechanics, a requested number of hurdles, and a difficulty value. The generator's job is not to create chaos. Its job is to create readable, solvable, systemic problems with clear consequences and hard choices.
+# Game Design: Reactor Maintenance
+
+## Core Concept
+
+The player controls a maintenance robot inside a collapsing industrial reactor facility.
+
+The game is a deterministic, turn-based systems puzzle about stabilizing interconnected machinery under pressure. The player does not fight enemies and does not react to hidden surprises. Instead, they study a visible machine, predict its failures, and choose which systems to save, reroute, isolate, or sacrifice.
+
+The facility is made of:
+
+- pipes
+- pressure systems
+- generators
+- locks and bulkhead doors
+- machines
+- diagnostics systems
+- reactor support systems
+
+Each turn has three phases:
+
+1. Player phase: the player spends a limited number of actions.
+2. Simulation phase: the facility updates all mechanical systems.
+3. Event phase: scheduled failures or generated hurdles advance.
+
+The main fantasy is:
+
+> "I understand this broken machine well enough to prevent the next disaster."
+
+The game should feel:
+
+- logical
+- tactical
+- deterministic
+- readable
+- systemic
+
+It should not feel:
+
+- hectic
+- random
+- action-heavy
+- dependent on hidden information
+
+## Goal
+
+The reactor starts offline.
+
+To win a level, the player must bring a required set of main systems online, then activate the reactor core.
+
+Example required systems:
+
+- cooling online
+- pressure stable
+- main power active
+
+When all required systems are ready, the game shows:
+
+> REACTOR READY
+
+The player can then spend an action at the reactor control terminal to activate the core and win.
+
+## Loss Conditions
+
+The player loses if a global failure state reaches zero or a hard safety limit is exceeded.
+
+Possible loss conditions:
+
+- core meltdown
+- pressure overflow
+- total power failure
+- facility stability collapse
+
+Important: failure should not be instant unless the player allows a clearly predicted critical event to happen. Most losses happen through slowly degrading global values, giving the player time to react.
+
+## Turn Structure
+
+### 1. Player Phase
+
+The player has a fixed number of actions per turn, usually 3.
+
+Possible actions:
+
+- move
+- repair pipe
+- remove pipe
+- rotate valve
+- lock or unlock bulkhead door
+- toggle breaker
+- connect cable
+- activate system
+- inspect diagnostic terminal
+
+### 2. Simulation Phase
+
+The facility updates all active systems.
+
+Examples:
+
+- pressure flows through connected pipes
+- leaks reduce cooling or pressure efficiency
+- generators produce power and consume fuel
+- machines consume power
+- cooling reduces heat
+- fire or overheating spreads through connected sectors
+- unstable pipes advance toward failure
+
+### 3. Event Phase
+
+Predicted events advance by one turn.
+
+Examples:
+
+- a leak gets worse
+- a short circuit disables a cable
+- an unstable pipe breaks
+- a door jams
+- a machine overheats
+- a pressure spike enters a sector
+
+Events are not hidden. The player sees upcoming failures before they happen.
+
+## Information Design
+
+All relevant information is visible and predictable.
+
+The player can see:
+
+- current pressure values
+- current power flow
+- current cooling flow
+- global stability values
+- unstable pipes
+- active leaks
+- locked and unlocked doors
+- scheduled upcoming failures
+- predicted sector consequences
+
+The game is not about asking:
+
+> "What is behind the corner?"
+
+It is about asking:
+
+> "What happens if I let this system run for two more turns?"
+
+## The Diagnostic Eye
+
+The "eye" theme is represented by industrial diagnostic systems.
+
+These can be:
+
+- mechanical observation lenses
+- wall-mounted diagnostic terminals
+- reactor monitoring cameras
+- pressure prediction displays
+
+The eye is not an enemy and not a stealth mechanic. It is the factory's analysis interface.
+
+Example diagnostic output:
+
+> PRESSURE FAILURE PREDICTED IN SECTOR C IN 2 TURNS
+
+## Core Gameplay Systems
+
+### Pipes
+
+Pipes transport pressure, coolant, or fuel.
+
+Pipes can be:
+
+- intact
+- leaking
+- unstable
+- broken
+- removed
+
+Player interactions:
+
+- repair a pipe
+- remove a pipe
+- route pressure through a different pipe network
+
+### Valves
+
+Valves control flow direction through pipe networks.
+
+Player interactions:
+
+- rotate valve
+- open valve
+- close valve
+
+Valves create tradeoffs. Rerouting pressure away from the reactor may stabilize the core but overload another sector.
+
+### Power
+
+Generators produce power. Machines consume power.
+
+Power flows through cables, breakers, and connected sectors.
+
+Player interactions:
+
+- toggle breaker
+- connect cable
+- activate generator
+- deactivate nonessential machine
+
+### Bulkhead Doors
+
+Bulkhead doors isolate sectors.
+
+Player interactions:
+
+- lock door
+- unlock door
+
+Locking a door can contain a disaster, but anything behind the door may become inaccessible.
+
+### Machines
+
+Machines provide level-critical functions.
+
+Examples:
+
+- cooling pump
+- pressure regulator
+- backup generator
+- reactor control terminal
+- diagnostic terminal
+
+Machines may need power, coolant, pressure, or a stable sector to function.
+
+## System Interaction Model
+
+The facility is modeled as a graph.
+
+- sectors are rooms or compact areas
+- pipes, cables, doors, and vents are edges between sectors
+- machines occupy sectors
+- hazards occupy sectors or edges
+- flow networks are computed from currently connected edges
+
+Each sector tracks a small set of readable values:
+
+| Value | Range | Meaning |
+| ----- | ----- | ------- |
+| Heat | 0-10 | Local temperature and fire risk |
+| Smoke | 0-10 | Visibility and movement penalty |
+| Fuel Vapor | 0-10 | Flammable gas concentration |
+| Coolant Pooling | 0-10 | Liquid coolant on the floor |
+| Electrical Charge | 0-10 | Exposed live current or arcing risk |
+| Stability | 0-10 | Structural and machinery integrity |
+
+Each pipe edge tracks:
+
+| Value | Range | Meaning |
+| ----- | ----- | ------- |
+| Medium | enum | pressure, coolant, or fuel |
+| Flow | 0-10 | Amount moving through the pipe this turn |
+| Pressure | 0-10 | Stress on the pipe |
+| Integrity | 0-10 | Damage before breaking |
+| Leak Rate | 0-10 | Amount lost into the sector each turn |
+
+The player never needs to inspect hidden formulas. The interface presents the result as direct predictions:
+
+- `FUEL VAPOR IGNITION IN 2 TURNS`
+- `COOLANT SHORT WILL DISABLE BREAKER NEXT TURN`
+- `SMOKE WILL COST +1 MOVE IN SECTOR D`
+- `PUMP CAVITATION: COOLING OUTPUT -2`
+
+### Flow Resolution
+
+During the simulation phase, each active network resolves in this order:
+
+1. Power networks energize machines, doors, breakers, and exposed cables.
+2. Fuel networks feed generators and leak into sectors.
+3. Pressure networks move stress through pipes and regulators.
+4. Coolant networks remove heat, pool through leaks, and feed pumps.
+5. Sector hazards update: heat, vapor, smoke, fire, charge, and stability.
+6. Machines update their output based on the final local conditions.
+
+This order makes cause and effect predictable. For example, a generator consumes fuel before it produces heat, then coolant removes some of that heat, then remaining heat may ignite vapor.
+
+### Fuel Leakage
+
+Fuel leaks are dangerous because fuel has two forms:
+
+- liquid fuel spilled in a sector
+- fuel vapor produced by heat, pressure spray, or accumulated spills
+
+Fuel leakage affects the facility in four ways:
+
+- lost fuel reduces generator runtime
+- fuel vapor creates ignition risk
+- liquid fuel slows movement
+- fire consumes fuel and spreads heat
+
+Simulation:
+
+- A leaking fuel pipe adds `Leak Rate` fuel to the sector each turn.
+- If sector heat is 4 or higher, some spilled fuel becomes vapor.
+- If pipe pressure is 7 or higher, leaked fuel sprays and becomes vapor faster.
+- Vapor decays slowly through ventilation or open doors.
+- Liquid fuel remains until drained, burned, sealed, or cleaned by a maintenance action.
+
+Player-solvable responses:
+
+- repair the fuel pipe
+- close an upstream fuel valve
+- reroute fuel through another pipe
+- depressurize the line before repairing it
+- ventilate the sector to reduce vapor
+- lock bulkheads to contain vapor
+- shut down ignition sources before entering or restarting machinery
+
+### Coolant Leakage
+
+Coolant leaks are less explosive than fuel leaks but can disable systems indirectly.
+
+Coolant leakage affects the facility in four ways:
+
+- less coolant reaches the reactor or overheated machines
+- pooled coolant conducts electricity
+- coolant vapor creates steam that reduces visibility
+- coolant contacting very hot machinery causes thermal shock
+
+Simulation:
+
+- A leaking coolant pipe subtracts from delivered coolant and adds `Coolant Pooling` to the local sector.
+- If coolant pooling touches exposed electrical charge, the sector becomes electrified.
+- If coolant pooling reaches heat 7 or higher, it becomes steam and increases smoke.
+- If a hot machine receives a sudden large coolant flow, its stability drops by 1 unless it is throttled through a regulator.
+
+Player-solvable responses:
+
+- repair or replace the coolant pipe
+- route coolant around the leak
+- cut power before walking through pooled coolant
+- open a drain before restarting cooling
+- throttle coolant flow into overheated machinery
+- use a bulkhead to isolate steam and preserve visibility elsewhere
+
+### Movement Implications
+
+Movement is not only a distance cost. The facility state changes how expensive or risky movement is.
+
+Sector movement modifiers:
+
+| Condition | Movement Effect | Extra Risk |
+| --------- | --------------- | ---------- |
+| Smoke 4+ | entering costs +1 move | diagnostic range reduced |
+| Smoke 8+ | entering costs +2 move | cannot inspect across sector |
+| Liquid Fuel 4+ | entering costs +1 move | ignition risk follows player tools |
+| Coolant Pooling 4+ | entering costs +1 move | electrocution if charged |
+| Active Fire | blocked unless shielded or suppressed | robot heat rises |
+| Electrified Floor | blocked unless power is cut or insulated | robot damage |
+| Low Pressure / Venting | entering costs +1 move | may push smoke or vapor |
+
+The robot is not fragile, but it should not ignore the environment. Hazards create routing puzzles:
+
+- the shortest path may become too slow
+- a safe path may require toggling breakers first
+- a locked door may protect the reactor but trap the robot away from tools
+- venting smoke may clear movement while spreading vapor into another sector
+
+### Conditions for Fire
+
+Fire starts only when fuel, oxygen, and ignition are all present.
+
+Fuel sources:
+
+- fuel vapor 4+
+- liquid fuel 6+
+- burning machine
+- overheated generator
+
+Oxygen sources:
+
+- normal unsealed sector
+- open vent
+- open bulkhead connection
+
+Ignition sources:
+
+- heat 8+
+- electrical arc
+- overloaded generator
+- reactor startup spark
+- pressure rupture through a fuel line
+
+Simulation:
+
+- If all three conditions are present, the sector gains `Fire`.
+- Fire adds heat each turn.
+- Fire converts nearby liquid fuel into vapor, then consumes it.
+- Fire damages machines and pipes in the sector.
+- Fire adds smoke to the sector and adjacent open sectors.
+- Fire cannot pass through a locked fire-rated bulkhead, but heat can slowly stress that door.
+- Fire dies when fuel is exhausted, oxygen is removed, or suppression exceeds fire intensity.
+
+Player-solvable responses:
+
+- remove fuel by closing valves or draining liquid
+- remove oxygen by sealing bulkheads or disabling vents
+- remove ignition by cutting power or cooling the sector
+- suppress fire using coolant mist, foam canisters, or vent purge systems
+- deliberately burn off fuel in an isolated sector when the reactor path must be cleared
+
+### Fuel and Coolant Interaction
+
+Fuel and coolant interact in useful and dangerous ways.
+
+- Coolant can reduce heat and prevent fuel ignition.
+- Coolant and fuel liquids don't mix.
+- Coolant mist and fuel vapor don't mix.
+- Coolant mist can suppress fire.
+- A coolant leak near a fuel leak can create a false sense of safety: ignition is delayed while cooling works, then becomes critical if coolant supply runs out.
+
+## Problematic States and Player-Solvable Hurdles
+
+A hurdle is a problematic state that blocks progress, threatens a loss condition, or changes the route the player can safely use. Every generated hurdle must have at least two viable responses unless the level is explicitly teaching a mechanic.
+
+### Fuel Leak Near Ignition Source
+
+Problem:
+
+A fuel pipe leaks into a sector containing an electrical arc, hot machine, or generator. Vapor will ignite after a visible countdown.
+
+System interactions:
+
+- fuel loss reduces generator runtime
+- vapor increases fire risk
+- liquid fuel slows movement
+- open doors or vents may spread vapor
+- coolant may delay ignition by lowering heat
+
+Viable solutions:
+
+- close the upstream fuel valve, then repair the pipe
+- cut power to the ignition source and vent the vapor
+- reroute fuel through a backup line
+- lock both bulkheads, allow controlled burnoff, then reopen after fire dies
+- activate coolant mist to keep heat below ignition while completing another task
+
+Simulation details:
+
+- Each turn, the pipe adds fuel equal to `Leak Rate`.
+- Heat converts some liquid fuel into vapor.
+- Vapor follows open airflow edges during hazard update.
+- Ignition countdown is calculated from vapor, heat, and active ignition sources.
+- Repairing under pressure costs an extra action or risks increasing leak rate by 1.
+
+### Coolant Leak Across Powered Cable
+
+Problem:
+
+A coolant leak is pooling across a live cable or breaker. The sector will become electrified, blocking movement and possibly disabling power.
+
+System interactions:
+
+- coolant delivery to the reactor is reduced
+- exposed power converts pooling into electrical charge
+- electrified coolant blocks or damages the robot
+- cutting power may shut down required machines
+
+Viable solutions:
+
+- toggle the breaker off, cross safely, repair the pipe, then restore power
+- reroute power around the wet sector
+- drain coolant before restoring the cable
+- reroute coolant through a dry pipe path
+- lock the sector and use a remote terminal to operate the machine beyond it
+
+Simulation details:
+
+- Pooling increases by leak rate each turn.
+- If a live cable exists and pooling is 3+, electrical charge rises.
+- At charge 4+, movement through the sector is blocked unless insulated.
+- At charge 7+, the breaker trips and the connected power network splits.
+- Drains reduce pooling at a fixed rate but may move coolant into lower sectors.
+
+### Fuel Leak Plus Coolant Leak
+
+Problem:
+
+Fuel and coolant leak into the same sector. The coolant suppresses heat temporarily, but the combined spill slows movement and can spread vapor through steam or drainage.
+
+System interactions:
+
+- coolant delays ignition while available
+- fuel vapor rises if heat climbs after coolant flow stops
+- drainage can spread liquid fuel to another sector
+- steam reduces visibility and increases movement cost
+- repairing either pipe changes the balance of the hazard
+
+Viable solutions:
+
+- repair fuel first if ignition risk is rising
+- repair coolant first if reactor heat is becoming critical
+- open drains only after isolating downstream sectors
+- vent steam after closing fuel valves
+- maintain coolant flow deliberately while rerouting fuel elsewhere
+
+Simulation details:
+
+- Fuel and coolant pooling are tracked separately.
+- Coolant reduces heat before vapor is produced.
+- If heat remains 7+ after cooling, steam rises and carries a fraction of vapor along vent paths.
+- Drainage moves a portion of both liquids unless a separator machine is powered.
+- The diagnostic display shows which repair has the larger predicted effect over the next two turns.
+
+### Fire Blocking the Critical Route
+
+Problem:
+
+An active fire blocks the shortest path to a required machine or terminal.
+
+System interactions:
+
+- fire raises heat and smoke
+- smoke increases movement cost and reduces diagnostic range
+- fire damages nearby pipes, possibly creating secondary leaks
+- bulkheads can contain fire but may cut off access
+- coolant can suppress fire but may cause electrical hazards
+
+Viable solutions:
+
+- seal bulkheads and take a longer route
+- cut power, then use coolant mist for suppression
+- vent smoke away from the route after removing fuel source
+- sacrifice the burning sector while powering the required machine remotely
+- drain fuel from the sector, wait for fire to consume remaining vapor, then pass
+
+Simulation details:
+
+- Fire intensity starts at 1-3 based on ignition strength.
+- Each turn, fire consumes fuel and adds heat and smoke.
+- If fuel remains and oxygen is present, intensity rises.
+- Suppression reduces intensity before spread is calculated.
+- Fire spreads only through open doors, vents, or flammable pipe ruptures.
+
+### Smoke-Filled Sector
+
+Problem:
+
+Smoke does not immediately destroy anything, but it makes movement slower and prediction weaker. The player may lose action efficiency before reaching a timed failure.
+
+System interactions:
+
+- fire creates smoke
+- steam from coolant leaks also counts as smoke
+- vents can clear smoke or move it
+- diagnostics become less useful through heavy smoke
+- smoke can hide floor hazards only if the player has not scanned the sector
+
+Viable solutions:
+
+- activate ventilation toward a safe exhaust path
+- open a bulkhead to disperse smoke, accepting spread risk
+- route around the sector
+- clear the originating fire or steam source
+- use a diagnostic terminal to inspect through smoke before entering
+
+Simulation details:
+
+- Smoke rises in sectors with fire or steam.
+- Ventilation moves smoke along active vent edges.
+- Closed bulkheads block smoke spread.
+- Smoke 4+ adds movement cost.
+- Smoke 8+ prevents long-range inspection through that sector.
+
+### Pressure Rupture in Fuel Line
+
+Problem:
+
+A pressurized fuel line is overloaded. If it ruptures, it will spray vapor instead of leaking liquid, creating a fast ignition threat.
+
+System interactions:
+
+- pressure stress damages the pipe
+- fuel vapor accumulates faster under pressure
+- venting pressure may push vapor into other sectors
+- shutting fuel may starve the generator
+- a pressure regulator can save the pipe but may overload a neighboring route
+
+Viable solutions:
+
+- rotate valves to reduce pressure before the rupture turn
+- vent pressure into a noncritical empty sector
+- shut down the generator fed by the line and close the fuel valve
+- reinforce the pipe to buy time, then reroute
+- isolate adjacent ignition sources before allowing a controlled rupture
+
+Simulation details:
+
+- Pipe integrity drops when pressure exceeds rated capacity.
+- At integrity 0, the pipe becomes broken and leak mode changes to spray.
+- Spray converts most leaked fuel directly into vapor.
+- Pressure venting reduces pipe stress but moves vapor along the selected vent path.
+- Reinforcement increases integrity but does not reduce pressure.
+
+### Generator Starved by Fuel Loss
+
+Problem:
+
+A required generator is online but its fuel supply is leaking. It will shut down before the player can activate the reactor.
+
+System interactions:
+
+- power output depends on fuel flow
+- low fuel causes brownouts, which can jam doors or stop pumps
+- repairing the leak may require moving through the leaking sector
+- routing more fuel may increase pressure and leak rate
+
+Viable solutions:
+
+- repair the fuel pipe
+- reduce power load by disabling nonessential machines
+- start a backup generator
+- reroute fuel through a lower-pressure path
+- store enough capacitor charge to bridge a short outage
+
+Simulation details:
+
+- Generator fuel buffer decreases when incoming fuel is below demand.
+- At low buffer, output becomes unstable and power flickers.
+- Flicker advances jam timers on powered doors and reduces pump efficiency.
+- Load shedding reduces fuel demand immediately.
+- Backup generators may produce less power but require fewer stable inputs.
+
+### Overheated Generator with Nearby Fuel
+
+Problem:
+
+A generator is producing needed power but heating a sector that contains fuel vapor or a leaking fuel pipe.
+
+System interactions:
+
+- power stability conflicts with fire safety
+- coolant can reduce heat but may short exposed cables
+- shutting the generator may disable the coolant pump
+- ventilation may cool the room but add oxygen
+
+Viable solutions:
+
+- throttle generator output and shed load
+- route coolant through a regulator before cooling the generator
+- repair the fuel leak before raising generator output
+- power the coolant pump from a backup source, then shut down the generator
+- seal the sector and run the generator only long enough to charge capacitors
+
+Simulation details:
+
+- Generator heat output scales with load.
+- If heat reaches 8 while vapor is 4+, ignition is predicted.
+- Coolant reduces heat after generator output, so undersized cooling may only delay ignition.
+- Load shedding lowers both heat and fuel consumption.
+- Capacitors store temporary power but discharge predictably each turn.
+
+### Coolant Starvation and Reactor Heat Rise
+
+Problem:
+
+The reactor needs cooling, but coolant is leaking, blocked, or diverted. Reactor heat rises toward meltdown.
+
+System interactions:
+
+- coolant leaks reduce delivered cooling
+- pressure can improve flow but stress pipes
+- power loss stops pumps
+- coolant sent to one overheated machine may starve the reactor
+- steam can block access to valves
+
+Viable solutions:
+
+- repair the leak on the main coolant route
+- reroute coolant through backup pipes
+- increase pump pressure briefly, accepting pipe stress elsewhere
+- throttle or shut down noncritical hot machines
+- use emergency coolant reserve for a short delay
+
+Simulation details:
+
+- Reactor heat increases by core instability minus delivered cooling.
+- Delivered cooling equals pump output minus leaks and blocked valves.
+- Pump output requires power and may require pressure.
+- Overpressure increases flow this turn but damages weak pipes.
+- Emergency reserve reduces heat immediately but has limited charges.
+
+### Electrified Bulkhead
+
+Problem:
+
+A bulkhead is needed for containment, but its controls are live, wet, or shorted. The player cannot safely lock or unlock it from the local panel.
+
+System interactions:
+
+- coolant or water pooling conducts charge
+- power loss may freeze the door in its current state
+- open door spreads smoke, vapor, fire, or pressure
+- closed door may trap the player away from required tools
+
+Viable solutions:
+
+- cut power to the door circuit, operate manual override, then restore power
+- drain the sector before touching the controls
+- reroute control power through another breaker
+- use a remote terminal if diagnostics are online
+- isolate hazards elsewhere so the door no longer needs to move
+
+Simulation details:
+
+- Door controls require power unless manual override is available.
+- If control panel charge is 4+, local operation is blocked.
+- If charge reaches 8, the door jams and requires repair.
+- Remote operation requires diagnostic network connectivity.
+- Manual override costs extra actions but ignores power state.
+
+### Jammed Door During Hazard Spread
+
+Problem:
+
+A door that should contain a hazard is jammed open or closed.
+
+System interactions:
+
+- jammed open allows smoke, vapor, pressure, or fire to spread
+- jammed closed can block the only path to a required system
+- low power or brownouts make jams worse
+- pressure difference across the door may prevent manual movement
+
+Viable solutions:
+
+- stabilize power, then operate the door
+- equalize pressure on both sides
+- repair the door actuator
+- reroute around the sector
+- create a different containment boundary using adjacent doors
+
+Simulation details:
+
+- Jam state has a severity from 1-3.
+- Each repair action reduces severity by 1 if pressure difference is safe.
+- Brownouts increase severity on powered doors with active faults.
+- High pressure difference blocks manual override.
+- A closed containment loop is valid even if it uses different doors than the obvious one.
+
+### Pressure Venting Through Hazardous Sector
+
+Problem:
+
+The safe pressure relief path crosses a sector with fuel vapor, fire, or coolant steam. Venting may solve pressure while spreading the hazard.
+
+System interactions:
+
+- pressure flow moves smoke and vapor
+- venting can add oxygen to a fire
+- pressure can push coolant pooling into lower sectors
+- not venting may burst a pipe
+
+Viable solutions:
+
+- clear the hazardous sector before venting
+- vent into a sealed sacrifice sector
+- rotate valves to use a lower-capacity but cleaner path
+- lock bulkheads downstream before opening the vent
+- reinforce the pressure pipe and delay venting until another route is ready
+
+Simulation details:
+
+- Venting reduces network pressure by a fixed amount.
+- Any vapor or smoke in the vent path is moved one sector per turn.
+- If venting feeds a fire with oxygen, fire intensity rises.
+- Sealed sacrifice sectors absorb pressure but lose stability.
+- Reinforced pipes can survive overpressure for a limited number of turns.
+
+### Unstable Pipe Chain Reaction
+
+Problem:
+
+Several damaged pipes are connected. Fixing only the first visible leak may increase flow through the next weak pipe and cause a later burst.
+
+System interactions:
+
+- repaired pipes restore flow and raise downstream pressure
+- valves can protect one branch by stressing another
+- coolant, fuel, and pressure networks may share physical sectors
+- burst pipes can create fire, steam, or movement hazards depending on medium
+
+Viable solutions:
+
+- repair the weakest downstream pipe before restoring full flow
+- throttle flow with a regulator
+- reroute around the damaged chain
+- isolate the branch and accept reduced system output
+- use temporary reinforcement to survive until reactor activation
+
+Simulation details:
+
+- Each pipe has rated pressure and integrity.
+- Flow restoration recalculates pressure across the whole network.
+- Diagnostics show the next predicted failure after a planned valve state.
+- Regulators cap flow but reduce delivered output.
+- Temporary reinforcement decays after a fixed number of turns.
+
+### Diagnostic Blind Spot
+
+Problem:
+
+A required prediction path is obscured by smoke, power loss, or a damaged diagnostic terminal. The player still knows the rules, but future consequences are less projected.
+
+System interactions:
+
+- smoke limits line-of-sight diagnostics
+- power loss disables terminals and cameras
+- fire or coolant shorts can damage sensors
+- blind spots make multi-turn planning harder but should not hide immediate lethal threats
+
+Viable solutions:
+
+- restore power to diagnostics
+- clear smoke or reroute camera feeds
+- repair the local sensor terminal
+- physically inspect the sector
+- use conservative containment: close valves, lock doors, and stabilize before proceeding
+
+Simulation details:
+
+- Blind sectors still show current visible hazards when entered.
+- Predictions beyond the blind sector are shortened or unavailable.
+- Repairing diagnostics restores full forecast for connected systems.
+- The generator must not place a mandatory invisible-only solution behind a blind spot.
+
+### Reactor Startup Hazard
+
+Problem:
+
+The reactor can be activated, but startup creates a predictable heat, pressure, and ignition pulse. Existing leaks can make the final activation unsafe.
+
+System interactions:
+
+- startup increases heat and pressure for one or more turns
+- fuel vapor in the reactor sector may ignite
+- weak coolant pipes may burst under final pump load
+- power demand may brown out doors or diagnostics
+
+Viable solutions:
+
+- pre-cool the reactor before activation
+- vent pressure immediately before startup
+- clear or seal fuel vapor in adjacent sectors
+- charge capacitors to cover startup power demand
+- lock containment doors so a noncritical sector absorbs the pulse
+
+Simulation details:
+
+- Startup applies a scripted but visible pulse profile.
+- The forecast panel shows startup consequences before the player commits.
+- If required systems are online but unstable, `REACTOR READY` becomes `REACTOR READY - UNSAFE STARTUP`.
+- Unsafe startup is allowed if it does not violate hard loss limits, preserving player agency.
+
+## Mechanic Simulation Reference
+
+The simulation should be simple enough to predict by inspection.
+
+### Heat
+
+- Heat rises from reactor instability, fire, overloaded generators, and hot machines.
+- Heat falls from coolant, ventilation, shutdown machinery, and time in stable sectors.
+- Heat 7+ accelerates fuel vapor creation.
+- Heat 8+ counts as an ignition source.
+- Heat 10 damages machines and pipes each turn.
+
+### Fuel
+
+- Fuel is consumed by generators.
+- Fuel can leak as liquid or spray as vapor.
+- Liquid fuel slows movement at high pooling values.
+- Vapor is the main fire risk.
+- Ventilation reduces vapor locally but may move it elsewhere.
+
+### Coolant
+
+- Coolant removes heat when delivered to a machine or sector.
+- Coolant leaks reduce delivered cooling and create pooling.
+- Pooling can conduct electricity.
+- Coolant contacting high heat creates steam.
+- Sudden coolant on overheated machinery can reduce stability unless regulated.
+
+### Power
+
+- Power is generated, routed, consumed, stored, and lost.
+- Brownouts do not immediately end the level; they reduce output, jam doors, and weaken pumps.
+- Exposed live power plus coolant pooling creates electrical charge.
+- Breakers define power network boundaries and are important player controls.
+
+### Pressure
+
+- Pressure moves useful flow but damages weak pipes.
+- Overpressure creates predicted bursts.
+- Pressure vents can save pipes but move airborne hazards.
+- Pressure differences affect door operation.
+- Regulators trade output for safety.
+
+### Fire and Smoke
+
+- Fire requires fuel, oxygen, and ignition.
+- Fire produces heat and smoke.
+- Smoke slows movement and shortens diagnostics.
+- Fire spreads through open routes and burns fuel.
+- Suppression, oxygen removal, or fuel removal can end fire.
+
+### Stability
+
+- Stability represents how close a sector, machine, or pipe is to mechanical failure.
+- Stability drops from heat, fire, pressure bursts, electrical shorts, and thermal shock.
+- Repairs restore local stability, not global safety by magic.
+- Global facility stability drops when critical sectors fail, when fire spreads, or when multiple machines break.
+
+## Hurdle Design Rules
+
+Every hurdle should be generated with:
+
+- a clear affected system
+- a visible countdown or consequence
+- at least one local solution
+- at least one systemic solution
+- one tradeoff that may create a secondary problem
+- a validation check that the player can physically reach or remotely affect the solution
+
+Local solutions are direct actions:
+
+- repair the pipe
+- toggle the breaker
+- rotate the valve
+- lock the door
+- drain the floor
+
+Systemic solutions change the wider machine:
+
+- reroute flow
+- shed load
+- sacrifice a sector
+- delay activation
+- isolate a hazard and let it burn out
+- use backup storage or reserves
+
+The generator should reject a hurdle if:
+
+- the only solution requires a disabled mechanic
+- a hard loss occurs before the player can take the minimum required actions
+- the solution requires crossing a blocked sector without an alternate route
+- the forecast cannot explain why the problem is happening
+- two hurdles combine into an unsolvable state before the player can respond
+
+## Procedural Level Generation
+
+Levels are generated deterministically from a compact generation request.
+
+
+## Generation Inputs
+
+### Seed
+
+The seed initializes the deterministic random number generator.
+
+### Enabled Mechanics
+
+The enabled mechanics list defines which systems may appear in the level.
+
+Example mechanics:
+
+- `power`
+- `cooling`
+- `doors`
+- `fuel`
+- `failures`
+
+The generator may only use mechanics from this list.
+
+If a mechanic is not enabled:
+
+- it must not be required for victory
+- it must not appear as an unresolved hurdle
+- it must not be needed to understand another generated system
+
+For every mechanic enabled:
+
+- the generator should include at least one meaningful use of that mechanic
+- the mechanic may appear as a direct hurdle, a solution path, or an interaction with another hurdle
+- if the mechanic is required for victory, the level must teach or expose its state before it becomes critical
+
+Mechanic requirements:
+
+| Mechanic | Required Presence | Example Hurdle | Example Solution Role |
+| -------- | ----------------- | -------------- | --------------------- |
+| `power` | at least one machine needs routed power | split power network | shed load to keep coolant pump online |
+| `cooling` | at least one heat source needs cooling | coolant starvation | suppress fire or protect generator |
+| `doors` | at least one sector boundary matters | jammed bulkhead | isolate vapor, fire, smoke, or pressure |
+| `fuel` | at least one generator or fuel pipe matters | fuel leak near ignition | choose between power uptime and fire safety |
+| `pressure` | at least one pipe network carries stress | pressure rupture | improve flow or vent hazard |
+| `failures` | at least one timed degradation occurs | pipe burst in 3 turns | repair, reroute, isolate, or delay |
+
+For every disabled mechanic, the level cannot generate associated tiles nor require associated actions.
+
+Disabled mechanics also limit interactions. For example:
+
+- if `fuel` is disabled, fire cannot require fuel-vapor reasoning
+- if `cooling` is disabled, overheated machines must be solvable through shutdown, venting, or power load changes
+- if `doors` are disabled, containment cannot require bulkhead locking
+- if `power` is disabled, machines may be manually operated, pressure-driven, or already powered by abstract local supply
+
+### Interaction Density
+
+The generator should distinguish primary hurdles from interaction consequences.
+
+Primary hurdle:
+
+- the problem counted by `Hurdle Count`
+- visible from the start or revealed by an explicit scheduled event
+- has a named forecast and at least two viable responses
+
+Interaction consequence:
+
+- emerges from simulation because of a primary hurdle or player solution
+- does not count against `Hurdle Count` unless it is deliberately generated as a primary hurdle
+- must be forecast before it becomes critical
+
+Example:
+
+- Primary hurdle: fuel pipe leak near Generator A.
+- Interaction consequence: coolant mist can suppress the heat but may electrify the wet floor if Breaker B remains on.
+
+The generator can deliberately pair mechanics:
+
+| Pair | Readable Interaction |
+| ---- | -------------------- |
+| fuel + power | generator uptime versus ignition risk |
+| fuel + cooling | cooling delays ignition but leaks may create steam |
+| cooling + power | coolant pooling creates electrical hazards |
+| pressure + fuel | rupture converts liquid leak into vapor spray |
+| pressure + doors | pressure difference blocks manual door operation |
+| fire + doors | containment protects the reactor but may block access |
+| smoke + diagnostics | predictions shorten until visibility is restored |
+
+Difficulty can increase interaction density, but it should not make every hurdle interact with every other hurdle. A readable level usually has one dominant interaction and one secondary interaction.
+
+### Hurdle Count
+
+Defines how many major problems the level contains.
+
+A hurdle is a meaningful obstacle that requires player attention, not just decorative damage.
+
+Examples:
+
+- a leaking pipe that prevents cooling from reaching the reactor
+- a pressure route that will burst in 3 turns unless rerouted
+- a locked door separating the player from a required generator
+- a short circuit splitting the power network
+- an overheating machine that must be cooled before activation
+- a sector that must be sacrificed to protect the core
+
+The generator should create an exact amount of primary hurdles.
+
+Secondary consequences may emerge from simulation, but the initial readable puzzle should have the requested number of main hurdles.
+
+### Difficulty
+
+Difficulty controls pressure on the player, not randomness.
+
+It can affect:
+
+- max move distance
+- starting global resource values
+- distance between important machines
+- urgency of scheduled failures
+- amount of spare power, coolant, or fuel
+- number of safe rerouting options
+- repair costs
+- density of damaged infrastructure
+- how many hurdles interact with each other
+
+Difficulty should not make the level less readable. Higher difficulty should create tighter tradeoffs and less slack.
+
+### Solvability Validation
+
+After generation, the level should run a deterministic validation pass.
+
+Validation checks:
+
+- every required system has a reachable activation path
+- every primary hurdle has at least two possible solution plans
+- no hard loss occurs before the player can take the shortest valid response
+- every forecasted hazard has at least one visible cause
+- generated interactions use only enabled mechanics
+- movement hazards do not trap the player without a remote solution or alternate route
+- final reactor startup has at least one safe or knowingly risky path
+
+The validator does not need to find the best solution. It only needs to prove that the level is not impossible, unreadable, or dependent on hidden information.
+
+## Hurdle Template Examples
+
+### Cooling Pipe Leak
+
+Problem:
+
+Cooling does not reach the reactor. Reactor heat rises every turn.
+
+Solutions:
+
+- repair the leaking pipe
+- reroute cooling through an alternate pipe path
+- activate backup cooling
+
+Difficulty modifiers:
+
+- higher difficulty places the leak farther from the player
+- higher difficulty adds a scheduled pipe burst
+- higher difficulty reduces starting cooling
+
+### Pressure Overload
+
+Problem:
+
+Pressure is building in a pipe network and will burst in a visible number of turns.
+
+Solutions:
+
+- rotate valve to redirect pressure
+- vent pressure into a noncritical sector
+- isolate the overloaded sector
+
+Difficulty modifiers:
+
+- higher difficulty creates consequences for every reroute
+- higher difficulty gives fewer safe vent paths
+
+### Split Power Network
+
+Problem:
+
+A required machine is not powered because the power network is split.
+
+Solutions:
+
+- reconnect cable
+- toggle breaker
+- activate backup generator
+- deactivate a nonessential machine to free capacity
+
+Difficulty modifiers:
+
+- higher difficulty reduces spare power
+- higher difficulty makes the player choose which machine stays offline
+
+### Jammed Bulkhead
+
+Problem:
+
+A sector must be isolated, but a door is jammed or unpowered.
+
+Solutions:
+
+- repair door control
+- restore power to the door
+- take a longer route to another door
+
+Difficulty modifiers:
+
+- higher difficulty adds a countdown before the hazard spreads
+- higher difficulty places useful resources behind the same door
+
+### Overheating Machine
+
+Problem:
+
+A required machine cannot be activated until heat is reduced.
+
+Solutions:
+
+- route cooling to the sector
+- reduce power load
+- activate venting system
+
+Difficulty modifiers:
+
+- higher difficulty links cooling to another already stressed system
+
+## Example Level: Cooling Sector B
+
+### Starting Global Values
+
+| System | Value |
+| ------ | ----- |
+| Power | 6/10 |
+| Cooling | 2/10 |
+| Pressure | 7/10 |
+| Heat | 6/10 |
+| Stability | 5/10 |
+
+### Hurdles
+
+1. Cooling pipe leak between the pump room and reactor chamber.
+2. Pressure overload in the maintenance shaft, predicted to burst in 3 turns.
+3. Bulkhead door can isolate the damaged sector, but doing so cuts off access to a generator.
+
+### Player Options
+
+#### Option A: Repair the Cooling Pipe
+
+Cost:
+
+- 2 actions
+
+Effect:
+
+- cooling reaches the reactor again
+
+Tradeoff:
+
+- the pressure overload remains unresolved
+
+#### Option B: Rotate the Pressure Valve
+
+Cost:
+
+- 1 action
+
+Effect:
+
+- reactor pressure stabilizes
+
+Tradeoff:
+
+- pressure rises in the maintenance shaft
+- pipe burst predicted there in 3 turns
+
+#### Option C: Lock the Bulkhead Door
+
+Cost:
+
+- 1 action
+
+Effect:
+
+- damaged sector is isolated
+- escalation stops there
+
+Tradeoff:
+
+- generator behind the door becomes unreachable
+- global power will slowly decline
+
+## Design Pillars
+
+### Deterministic Systems
+
+The same actions in the same state always produce the same outcome.
+
+### Visible Consequences
+
+The player should see upcoming failures and understand why they will happen.
+
+### Meaningful Tradeoffs
+
+The player should often solve one problem by making another problem worse.
+
+### Procedural but Authored-Feeling
+
+Generated levels should feel intentionally designed. The generator should use templates, validation, and readable system graphs instead of placing random damage everywhere.
+
+### Resource Prioritization
+
+The real puzzle is not a single lock-and-key solution. It is deciding which systems matter most and which losses are acceptable.
+
+## Summary
+
+This is an industrial chain-reaction puzzle game.
+
+The player wins by understanding a deterministic machine under stress. Procedural generation creates replayable levels from a seed, a list of enabled mechanics, a requested number of hurdles, and a difficulty value. The generator's job is not to create chaos. Its job is to create readable, solvable, systemic problems with clear consequences and hard choices.
diff --git a/src/ReactorMaintenance.Simulation/Balancing.cs b/src/ReactorMaintenance.Simulation/Balancing.cs
index 890bb46..6c61a62 100644
--- a/src/ReactorMaintenance.Simulation/Balancing.cs
+++ b/src/ReactorMaintenance.Simulation/Balancing.cs
@@ -1,78 +1,78 @@
-using ReactorMaintenance.Simulation.Difficulties;
-
-namespace ReactorMaintenance.Simulation;
-
-public abstract class Balancing
-{
- public static Balancing Current { get; set; } = new NormalBalancing();
-
- public abstract int MinHazardValue { get; }
- public abstract int MaxHazardValue { get; }
- public abstract int DefaultHazardStability { get; }
- public abstract int DefaultCellIntegrity { get; }
- public abstract int DefaultActionsPerTurn { get; }
- public abstract int DefaultCoreHeat { get; }
- public abstract int DefaultFacilityStability { get; }
- public abstract int DefaultPower { get; }
- public abstract int DefaultCooling { get; }
- public abstract int FirstGridCoordinate { get; }
- public abstract int NeighborDistance { get; }
- public abstract int CurrentForecastTurn { get; }
- public abstract int MinimumLevelSize { get; }
- public abstract int DefaultLevelWidth { get; }
- public abstract int DefaultLevelHeight { get; }
- public abstract int DefaultRobotCoordinate { get; }
- public abstract int DefaultPipeFlow { get; }
- public abstract int DefaultPipePressure { get; }
- public abstract int DefaultPressurePipeFlow { get; }
- public abstract int DefaultPressurePipePressure { get; }
- public abstract int DefaultEditedPipeIntegrity { get; }
- public abstract int MinimumLeakRate { get; }
- public abstract int DamagedPipeIntegrity { get; }
- public abstract int RepairedLeakRate { get; }
- public abstract int RepairedElectricalCharge { get; }
- public abstract int HeatToolIncrease { get; }
- public abstract int FireToolMinimumHeat { get; }
- public abstract int FireToolMinimumSmoke { get; }
- public abstract int MaxForecastStepCount { get; }
- public abstract int TurnIncrement { get; }
- public abstract int OverpressureThreshold { get; }
- public abstract int HeatIntegrityDamageThreshold { get; }
- public abstract int PipeFireIntegrityDamage { get; }
- public abstract int FireStabilityDamage { get; }
- public abstract int BurstLeakRate { get; }
- public abstract int BrokenPipeFlow { get; }
- public abstract int ElectrifiedCoolantPoolingThreshold { get; }
- public abstract int ElectricalChargeIncrease { get; }
- public abstract int FuelVaporFireThreshold { get; }
- public abstract int LiquidFuelFireThreshold { get; }
- public abstract int HeatIgnitionThreshold { get; }
- public abstract int ElectricalIgnitionThreshold { get; }
- public abstract int FireHeatIncrease { get; }
- public abstract int FireSmokeIncrease { get; }
- public abstract int FireLiquidFuelConsumption { get; }
- public abstract int FireFuelVaporConsumption { get; }
- public abstract int SmokeDecay { get; }
- public abstract int PressurizedFuelLeakPressureThreshold { get; }
- public abstract int PassiveFuelVaporHeatOffset { get; }
- public abstract int PassiveFuelVaporDivisor { get; }
- public abstract int MinimumCoolantHeatReduction { get; }
- public abstract int CoolantHeatReductionDivisor { get; }
- public abstract int CoolantSteamHeatThreshold { get; }
- public abstract int CoolantSteamSmokeIncrease { get; }
- public abstract int PressureLeakSmokeThreshold { get; }
- public abstract int PressureLeakSmokeIncrease { get; }
- public abstract int GeneratorHeatIncrease { get; }
- public abstract int CoolingPumpHeatReduction { get; }
- public abstract int ReactorHeatIncrease { get; }
- public abstract int SmokeSpreadThreshold { get; }
- public abstract int SmokeSpreadIncrease { get; }
- public abstract int CriticalCellStabilityThreshold { get; }
- public abstract int MeltdownCoreHeatThreshold { get; }
- public abstract int StabilityCollapseThreshold { get; }
- public abstract int GeneratorPowerOutput { get; }
- public abstract int CoolingPumpOutput { get; }
- public abstract int ReactorReadyPowerThreshold { get; }
- public abstract int ReactorReadyCoolingThreshold { get; }
- public abstract int ReactorReadyCoreHeatThreshold { get; }
+using ReactorMaintenance.Simulation.Difficulties;
+
+namespace ReactorMaintenance.Simulation;
+
+public abstract class Balancing
+{
+ public static Balancing Current { get; set; } = new NormalBalancing();
+
+ public abstract int MinHazardValue { get; }
+ public abstract int MaxHazardValue { get; }
+ public abstract int DefaultHazardStability { get; }
+ public abstract int DefaultCellIntegrity { get; }
+ public abstract int DefaultActionsPerTurn { get; }
+ public abstract int DefaultCoreHeat { get; }
+ public abstract int DefaultFacilityStability { get; }
+ public abstract int DefaultPower { get; }
+ public abstract int DefaultCooling { get; }
+ public abstract int FirstGridCoordinate { get; }
+ public abstract int NeighborDistance { get; }
+ public abstract int CurrentForecastTurn { get; }
+ public abstract int MinimumLevelSize { get; }
+ public abstract int DefaultLevelWidth { get; }
+ public abstract int DefaultLevelHeight { get; }
+ public abstract int DefaultRobotCoordinate { get; }
+ public abstract int DefaultPipeFlow { get; }
+ public abstract int DefaultPipePressure { get; }
+ public abstract int DefaultPressurePipeFlow { get; }
+ public abstract int DefaultPressurePipePressure { get; }
+ public abstract int DefaultEditedPipeIntegrity { get; }
+ public abstract int MinimumLeakRate { get; }
+ public abstract int DamagedPipeIntegrity { get; }
+ public abstract int RepairedLeakRate { get; }
+ public abstract int RepairedElectricalCharge { get; }
+ public abstract int HeatToolIncrease { get; }
+ public abstract int FireToolMinimumHeat { get; }
+ public abstract int FireToolMinimumSmoke { get; }
+ public abstract int MaxForecastStepCount { get; }
+ public abstract int TurnIncrement { get; }
+ public abstract int OverpressureThreshold { get; }
+ public abstract int HeatIntegrityDamageThreshold { get; }
+ public abstract int PipeFireIntegrityDamage { get; }
+ public abstract int FireStabilityDamage { get; }
+ public abstract int BurstLeakRate { get; }
+ public abstract int BrokenPipeFlow { get; }
+ public abstract int ElectrifiedCoolantPoolingThreshold { get; }
+ public abstract int ElectricalChargeIncrease { get; }
+ public abstract int FuelVaporFireThreshold { get; }
+ public abstract int LiquidFuelFireThreshold { get; }
+ public abstract int HeatIgnitionThreshold { get; }
+ public abstract int ElectricalIgnitionThreshold { get; }
+ public abstract int FireHeatIncrease { get; }
+ public abstract int FireSmokeIncrease { get; }
+ public abstract int FireLiquidFuelConsumption { get; }
+ public abstract int FireFuelVaporConsumption { get; }
+ public abstract int SmokeDecay { get; }
+ public abstract int PressurizedFuelLeakPressureThreshold { get; }
+ public abstract int PassiveFuelVaporHeatOffset { get; }
+ public abstract int PassiveFuelVaporDivisor { get; }
+ public abstract int MinimumCoolantHeatReduction { get; }
+ public abstract int CoolantHeatReductionDivisor { get; }
+ public abstract int CoolantSteamHeatThreshold { get; }
+ public abstract int CoolantSteamSmokeIncrease { get; }
+ public abstract int PressureLeakSmokeThreshold { get; }
+ public abstract int PressureLeakSmokeIncrease { get; }
+ public abstract int GeneratorHeatIncrease { get; }
+ public abstract int CoolingPumpHeatReduction { get; }
+ public abstract int ReactorHeatIncrease { get; }
+ public abstract int SmokeSpreadThreshold { get; }
+ public abstract int SmokeSpreadIncrease { get; }
+ public abstract int CriticalCellStabilityThreshold { get; }
+ public abstract int MeltdownCoreHeatThreshold { get; }
+ public abstract int StabilityCollapseThreshold { get; }
+ public abstract int GeneratorPowerOutput { get; }
+ public abstract int CoolingPumpOutput { get; }
+ public abstract int ReactorReadyPowerThreshold { get; }
+ public abstract int ReactorReadyCoolingThreshold { get; }
+ public abstract int ReactorReadyCoreHeatThreshold { get; }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs b/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs
index 40572cb..e42b020 100644
--- a/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs
+++ b/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs
@@ -1,76 +1,76 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Difficulties;
-
-public class NormalBalancing : Balancing
-{
- public override int MinHazardValue => 0;
- public override int MaxHazardValue => 10;
- public override int DefaultHazardStability => 10;
- public override int DefaultCellIntegrity => 10;
- public override int DefaultActionsPerTurn => 3;
- public override int DefaultCoreHeat => 5;
- public override int DefaultFacilityStability => 10;
- public override int DefaultPower => 5;
- public override int DefaultCooling => 0;
- public override int FirstGridCoordinate => 0;
- public override int NeighborDistance => 1;
- public override int CurrentForecastTurn => 0;
- public override int MinimumLevelSize => 4;
- public override int DefaultLevelWidth => 16;
- public override int DefaultLevelHeight => 12;
- public override int DefaultRobotCoordinate => 1;
- public override int DefaultPipeFlow => 4;
- public override int DefaultPipePressure => 4;
- public override int DefaultPressurePipeFlow => 5;
- public override int DefaultPressurePipePressure => 6;
- public override int DefaultEditedPipeIntegrity => 8;
- public override int MinimumLeakRate => 1;
- public override int DamagedPipeIntegrity => 4;
- public override int RepairedLeakRate => 0;
- public override int RepairedElectricalCharge => 0;
- public override int HeatToolIncrease => 2;
- public override int FireToolMinimumHeat => 7;
- public override int FireToolMinimumSmoke => 3;
- public override int MaxForecastStepCount => 12;
- public override int TurnIncrement => 1;
- public override int OverpressureThreshold => 7;
- public override int HeatIntegrityDamageThreshold => 10;
- public override int PipeFireIntegrityDamage => 1;
- public override int FireStabilityDamage => 1;
- public override int BurstLeakRate => 3;
- public override int BrokenPipeFlow => 0;
- public override int ElectrifiedCoolantPoolingThreshold => 3;
- public override int ElectricalChargeIncrease => 2;
- public override int FuelVaporFireThreshold => 4;
- public override int LiquidFuelFireThreshold => 6;
- public override int HeatIgnitionThreshold => 8;
- public override int ElectricalIgnitionThreshold => 4;
- public override int FireHeatIncrease => 2;
- public override int FireSmokeIncrease => 2;
- public override int FireLiquidFuelConsumption => 1;
- public override int FireFuelVaporConsumption => 1;
- public override int SmokeDecay => 1;
- public override int PressurizedFuelLeakPressureThreshold => 7;
- public override int PassiveFuelVaporHeatOffset => 3;
- public override int PassiveFuelVaporDivisor => 3;
- public override int MinimumCoolantHeatReduction => 1;
- public override int CoolantHeatReductionDivisor => 2;
- public override int CoolantSteamHeatThreshold => 7;
- public override int CoolantSteamSmokeIncrease => 2;
- public override int PressureLeakSmokeThreshold => 8;
- public override int PressureLeakSmokeIncrease => 1;
- public override int GeneratorHeatIncrease => 1;
- public override int CoolingPumpHeatReduction => 2;
- public override int ReactorHeatIncrease => 1;
- public override int SmokeSpreadThreshold => 6;
- public override int SmokeSpreadIncrease => 1;
- public override int CriticalCellStabilityThreshold => 3;
- public override int MeltdownCoreHeatThreshold => 10;
- public override int StabilityCollapseThreshold => 0;
- public override int GeneratorPowerOutput => 3;
- public override int CoolingPumpOutput => 3;
- public override int ReactorReadyPowerThreshold => 3;
- public override int ReactorReadyCoolingThreshold => 3;
- public override int ReactorReadyCoreHeatThreshold => 8;
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Difficulties;
+
+public class NormalBalancing : Balancing
+{
+ public override int MinHazardValue => 0;
+ public override int MaxHazardValue => 10;
+ public override int DefaultHazardStability => 10;
+ public override int DefaultCellIntegrity => 10;
+ public override int DefaultActionsPerTurn => 3;
+ public override int DefaultCoreHeat => 5;
+ public override int DefaultFacilityStability => 10;
+ public override int DefaultPower => 5;
+ public override int DefaultCooling => 0;
+ public override int FirstGridCoordinate => 0;
+ public override int NeighborDistance => 1;
+ public override int CurrentForecastTurn => 0;
+ public override int MinimumLevelSize => 4;
+ public override int DefaultLevelWidth => 16;
+ public override int DefaultLevelHeight => 12;
+ public override int DefaultRobotCoordinate => 1;
+ public override int DefaultPipeFlow => 4;
+ public override int DefaultPipePressure => 4;
+ public override int DefaultPressurePipeFlow => 5;
+ public override int DefaultPressurePipePressure => 6;
+ public override int DefaultEditedPipeIntegrity => 8;
+ public override int MinimumLeakRate => 1;
+ public override int DamagedPipeIntegrity => 4;
+ public override int RepairedLeakRate => 0;
+ public override int RepairedElectricalCharge => 0;
+ public override int HeatToolIncrease => 2;
+ public override int FireToolMinimumHeat => 7;
+ public override int FireToolMinimumSmoke => 3;
+ public override int MaxForecastStepCount => 12;
+ public override int TurnIncrement => 1;
+ public override int OverpressureThreshold => 7;
+ public override int HeatIntegrityDamageThreshold => 10;
+ public override int PipeFireIntegrityDamage => 1;
+ public override int FireStabilityDamage => 1;
+ public override int BurstLeakRate => 3;
+ public override int BrokenPipeFlow => 0;
+ public override int ElectrifiedCoolantPoolingThreshold => 3;
+ public override int ElectricalChargeIncrease => 2;
+ public override int FuelVaporFireThreshold => 4;
+ public override int LiquidFuelFireThreshold => 6;
+ public override int HeatIgnitionThreshold => 8;
+ public override int ElectricalIgnitionThreshold => 4;
+ public override int FireHeatIncrease => 2;
+ public override int FireSmokeIncrease => 2;
+ public override int FireLiquidFuelConsumption => 1;
+ public override int FireFuelVaporConsumption => 1;
+ public override int SmokeDecay => 1;
+ public override int PressurizedFuelLeakPressureThreshold => 7;
+ public override int PassiveFuelVaporHeatOffset => 3;
+ public override int PassiveFuelVaporDivisor => 3;
+ public override int MinimumCoolantHeatReduction => 1;
+ public override int CoolantHeatReductionDivisor => 2;
+ public override int CoolantSteamHeatThreshold => 7;
+ public override int CoolantSteamSmokeIncrease => 2;
+ public override int PressureLeakSmokeThreshold => 8;
+ public override int PressureLeakSmokeIncrease => 1;
+ public override int GeneratorHeatIncrease => 1;
+ public override int CoolingPumpHeatReduction => 2;
+ public override int ReactorHeatIncrease => 1;
+ public override int SmokeSpreadThreshold => 6;
+ public override int SmokeSpreadIncrease => 1;
+ public override int CriticalCellStabilityThreshold => 3;
+ public override int MeltdownCoreHeatThreshold => 10;
+ public override int StabilityCollapseThreshold => 0;
+ public override int GeneratorPowerOutput => 3;
+ public override int CoolingPumpOutput => 3;
+ public override int ReactorReadyPowerThreshold => 3;
+ public override int ReactorReadyCoolingThreshold => 3;
+ public override int ReactorReadyCoreHeatThreshold => 8;
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Effects/CellIntegrityEffect.cs b/src/ReactorMaintenance.Simulation/Effects/CellIntegrityEffect.cs
index d32ad13..0e5ffa8 100644
--- a/src/ReactorMaintenance.Simulation/Effects/CellIntegrityEffect.cs
+++ b/src/ReactorMaintenance.Simulation/Effects/CellIntegrityEffect.cs
@@ -1,35 +1,35 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Effects;
-
-public sealed class CellIntegrityEffect : ISimulationEffect
-{
- public CellState Apply(CellState cell)
- {
- var integrity = cell.Integrity;
- var hazards = cell.Hazards;
-
- if (cell is { HasPipe: true } && cell.Pressure > Balancing.Current.OverpressureThreshold)
- integrity -= cell.Pressure - Balancing.Current.OverpressureThreshold;
-
- if (hazards.Heat >= Balancing.Current.HeatIntegrityDamageThreshold || hazards.Fire)
- {
- integrity -= cell.HasPipe ? Balancing.Current.PipeFireIntegrityDamage : Balancing.Current.MinHazardValue;
- hazards = hazards with { Stability = hazards.Stability - Balancing.Current.FireStabilityDamage };
- }
-
- cell = cell with {
- Integrity = Rules.Clamp(integrity),
- Hazards = hazards.Clamp()
- };
-
- if (integrity > Balancing.Current.MinHazardValue || !cell.HasPipe)
- return cell;
-
- return cell with {
- LeakRate = Math.Max(cell.LeakRate, Balancing.Current.BurstLeakRate),
- Flow = Balancing.Current.BrokenPipeFlow,
- PipeOpen = false
- };
- }
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Effects;
+
+public sealed class CellIntegrityEffect : ISimulationEffect
+{
+ public CellState Apply(CellState cell)
+ {
+ var integrity = cell.Integrity;
+ var hazards = cell.Hazards;
+
+ if (cell is { HasPipe: true } && cell.Pressure > Balancing.Current.OverpressureThreshold)
+ integrity -= cell.Pressure - Balancing.Current.OverpressureThreshold;
+
+ if (hazards.Heat >= Balancing.Current.HeatIntegrityDamageThreshold || hazards.Fire)
+ {
+ integrity -= cell.HasPipe ? Balancing.Current.PipeFireIntegrityDamage : Balancing.Current.MinHazardValue;
+ hazards = hazards with { Stability = hazards.Stability - Balancing.Current.FireStabilityDamage };
+ }
+
+ cell = cell with {
+ Integrity = Rules.Clamp(integrity),
+ Hazards = hazards.Clamp()
+ };
+
+ if (integrity > Balancing.Current.MinHazardValue || !cell.HasPipe)
+ return cell;
+
+ return cell with {
+ LeakRate = Math.Max(cell.LeakRate, Balancing.Current.BurstLeakRate),
+ Flow = Balancing.Current.BrokenPipeFlow,
+ PipeOpen = false
+ };
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs b/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs
index 283e7e0..5e0ec88 100644
--- a/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs
+++ b/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs
@@ -1,30 +1,30 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Effects;
-
-public sealed class FireAndElectricalHazardEffect : ISimulationEffect
-{
- public CellState Apply(CellState cell)
- {
- var hazards = cell.Hazards;
- if (hazards.CoolantPooling >= Balancing.Current.ElectrifiedCoolantPoolingThreshold && cell.Powered)
- hazards = hazards with { ElectricalCharge = hazards.ElectricalCharge + Balancing.Current.ElectricalChargeIncrease };
-
- var hasFuel = hazards.FuelVapor >= Balancing.Current.FuelVaporFireThreshold || hazards.LiquidFuel >= Balancing.Current.LiquidFuelFireThreshold;
- var hasIgnition = hazards.Heat >= Balancing.Current.HeatIgnitionThreshold || hazards.ElectricalCharge >= Balancing.Current.ElectricalIgnitionThreshold || cell is { Prop: ECellProp.Generator, Powered: true };
- if ((hasFuel && hasIgnition) || hazards.Fire)
- {
- hazards = hazards with {
- Fire = hasFuel || hazards.Fire,
- Heat = hazards.Heat + Balancing.Current.FireHeatIncrease,
- Smoke = hazards.Smoke + Balancing.Current.FireSmokeIncrease,
- LiquidFuel = Math.Max(Balancing.Current.MinHazardValue, hazards.LiquidFuel - Balancing.Current.FireLiquidFuelConsumption),
- FuelVapor = Math.Max(Balancing.Current.MinHazardValue, hazards.FuelVapor - Balancing.Current.FireFuelVaporConsumption)
- };
- }
- else if (hazards.Smoke > Balancing.Current.MinHazardValue)
- hazards = hazards with { Smoke = hazards.Smoke - Balancing.Current.SmokeDecay };
-
- return cell with { Hazards = hazards.Clamp() };
- }
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Effects;
+
+public sealed class FireAndElectricalHazardEffect : ISimulationEffect
+{
+ public CellState Apply(CellState cell)
+ {
+ var hazards = cell.Hazards;
+ if (hazards.CoolantPooling >= Balancing.Current.ElectrifiedCoolantPoolingThreshold && cell.Powered)
+ hazards = hazards with { ElectricalCharge = hazards.ElectricalCharge + Balancing.Current.ElectricalChargeIncrease };
+
+ var hasFuel = hazards.FuelVapor >= Balancing.Current.FuelVaporFireThreshold || hazards.LiquidFuel >= Balancing.Current.LiquidFuelFireThreshold;
+ var hasIgnition = hazards.Heat >= Balancing.Current.HeatIgnitionThreshold || hazards.ElectricalCharge >= Balancing.Current.ElectricalIgnitionThreshold || cell is { Prop: ECellProp.Generator, Powered: true };
+ if ((hasFuel && hasIgnition) || hazards.Fire)
+ {
+ hazards = hazards with {
+ Fire = hasFuel || hazards.Fire,
+ Heat = hazards.Heat + Balancing.Current.FireHeatIncrease,
+ Smoke = hazards.Smoke + Balancing.Current.FireSmokeIncrease,
+ LiquidFuel = Math.Max(Balancing.Current.MinHazardValue, hazards.LiquidFuel - Balancing.Current.FireLiquidFuelConsumption),
+ FuelVapor = Math.Max(Balancing.Current.MinHazardValue, hazards.FuelVapor - Balancing.Current.FireFuelVaporConsumption)
+ };
+ }
+ else if (hazards.Smoke > Balancing.Current.MinHazardValue)
+ hazards = hazards with { Smoke = hazards.Smoke - Balancing.Current.SmokeDecay };
+
+ return cell with { Hazards = hazards.Clamp() };
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Effects/IAreaSimulationEffect.cs b/src/ReactorMaintenance.Simulation/Effects/IAreaSimulationEffect.cs
index 4bba0cc..0dd929c 100644
--- a/src/ReactorMaintenance.Simulation/Effects/IAreaSimulationEffect.cs
+++ b/src/ReactorMaintenance.Simulation/Effects/IAreaSimulationEffect.cs
@@ -1,8 +1,8 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Effects;
-
-public interface IAreaSimulationEffect
-{
- CellState[] Apply(LevelState level, CellState[] cells);
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Effects;
+
+public interface IAreaSimulationEffect
+{
+ CellState[] Apply(LevelState level, CellState[] cells);
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Effects/ISimulationEffect.cs b/src/ReactorMaintenance.Simulation/Effects/ISimulationEffect.cs
index 4d6a1b5..8811633 100644
--- a/src/ReactorMaintenance.Simulation/Effects/ISimulationEffect.cs
+++ b/src/ReactorMaintenance.Simulation/Effects/ISimulationEffect.cs
@@ -1,8 +1,8 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Effects;
-
-public interface ISimulationEffect
-{
- CellState Apply(CellState cell);
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Effects;
+
+public interface ISimulationEffect
+{
+ CellState Apply(CellState cell);
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs b/src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs
index 5a070dc..f9ba603 100644
--- a/src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs
+++ b/src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs
@@ -1,18 +1,18 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Effects;
-
-public sealed class MachineEffect : ISimulationEffect
-{
- public CellState Apply(CellState cell)
- {
- var hazards = cell.Prop switch {
- ECellProp.Generator when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.GeneratorHeatIncrease },
- ECellProp.CoolingPump when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat - Balancing.Current.CoolingPumpHeatReduction },
- ECellProp.Reactor => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.ReactorHeatIncrease },
- _ => cell.Hazards
- };
-
- return cell with { Hazards = hazards.Clamp() };
- }
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Effects;
+
+public sealed class MachineEffect : ISimulationEffect
+{
+ public CellState Apply(CellState cell)
+ {
+ var hazards = cell.Prop switch {
+ ECellProp.Generator when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.GeneratorHeatIncrease },
+ ECellProp.CoolingPump when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat - Balancing.Current.CoolingPumpHeatReduction },
+ ECellProp.Reactor => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.ReactorHeatIncrease },
+ _ => cell.Hazards
+ };
+
+ return cell with { Hazards = hazards.Clamp() };
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Effects/PipeLeakEffect.cs b/src/ReactorMaintenance.Simulation/Effects/PipeLeakEffect.cs
index 458f24a..3e2b213 100644
--- a/src/ReactorMaintenance.Simulation/Effects/PipeLeakEffect.cs
+++ b/src/ReactorMaintenance.Simulation/Effects/PipeLeakEffect.cs
@@ -1,28 +1,28 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Effects;
-
-public sealed class PipeLeakEffect : ISimulationEffect
-{
- public CellState Apply(CellState cell)
- {
- if (!cell.HasPipe || cell.LeakRate <= Balancing.Current.MinHazardValue)
- return cell;
-
- var hazards = cell.Pipe switch {
- EPipeMedium.Fuel => cell.Hazards with {
- LiquidFuel = cell.Hazards.LiquidFuel + cell.LeakRate,
- FuelVapor = cell.Hazards.FuelVapor + (cell.Pressure >= Balancing.Current.PressurizedFuelLeakPressureThreshold ? cell.LeakRate : Math.Max(Balancing.Current.MinHazardValue, cell.Hazards.Heat - Balancing.Current.PassiveFuelVaporHeatOffset) / Balancing.Current.PassiveFuelVaporDivisor)
- },
- EPipeMedium.Coolant => cell.Hazards with {
- CoolantPooling = cell.Hazards.CoolantPooling + cell.LeakRate,
- Heat = cell.Hazards.Heat - Math.Max(Balancing.Current.MinimumCoolantHeatReduction, cell.LeakRate / Balancing.Current.CoolantHeatReductionDivisor),
- Smoke = cell.Hazards.Smoke + (cell.Hazards.Heat >= Balancing.Current.CoolantSteamHeatThreshold ? Balancing.Current.CoolantSteamSmokeIncrease : Balancing.Current.MinHazardValue)
- },
- EPipeMedium.Pressure => cell.Hazards with { Smoke = cell.Hazards.Smoke + (cell.Pressure >= Balancing.Current.PressureLeakSmokeThreshold ? Balancing.Current.PressureLeakSmokeIncrease : Balancing.Current.MinHazardValue) },
- _ => cell.Hazards
- };
-
- return cell with { Hazards = hazards.Clamp() };
- }
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Effects;
+
+public sealed class PipeLeakEffect : ISimulationEffect
+{
+ public CellState Apply(CellState cell)
+ {
+ if (!cell.HasPipe || cell.LeakRate <= Balancing.Current.MinHazardValue)
+ return cell;
+
+ var hazards = cell.Pipe switch {
+ EPipeMedium.Fuel => cell.Hazards with {
+ LiquidFuel = cell.Hazards.LiquidFuel + cell.LeakRate,
+ FuelVapor = cell.Hazards.FuelVapor + (cell.Pressure >= Balancing.Current.PressurizedFuelLeakPressureThreshold ? cell.LeakRate : Math.Max(Balancing.Current.MinHazardValue, cell.Hazards.Heat - Balancing.Current.PassiveFuelVaporHeatOffset) / Balancing.Current.PassiveFuelVaporDivisor)
+ },
+ EPipeMedium.Coolant => cell.Hazards with {
+ CoolantPooling = cell.Hazards.CoolantPooling + cell.LeakRate,
+ Heat = cell.Hazards.Heat - Math.Max(Balancing.Current.MinimumCoolantHeatReduction, cell.LeakRate / Balancing.Current.CoolantHeatReductionDivisor),
+ Smoke = cell.Hazards.Smoke + (cell.Hazards.Heat >= Balancing.Current.CoolantSteamHeatThreshold ? Balancing.Current.CoolantSteamSmokeIncrease : Balancing.Current.MinHazardValue)
+ },
+ EPipeMedium.Pressure => cell.Hazards with { Smoke = cell.Hazards.Smoke + (cell.Pressure >= Balancing.Current.PressureLeakSmokeThreshold ? Balancing.Current.PressureLeakSmokeIncrease : Balancing.Current.MinHazardValue) },
+ _ => cell.Hazards
+ };
+
+ return cell with { Hazards = hazards.Clamp() };
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Effects/SmokeSpreadEffect.cs b/src/ReactorMaintenance.Simulation/Effects/SmokeSpreadEffect.cs
index 432d9f1..80c185d 100644
--- a/src/ReactorMaintenance.Simulation/Effects/SmokeSpreadEffect.cs
+++ b/src/ReactorMaintenance.Simulation/Effects/SmokeSpreadEffect.cs
@@ -1,37 +1,37 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Effects;
-
-public sealed class SmokeSpreadEffect : IAreaSimulationEffect
-{
- public CellState[] Apply(LevelState level, CellState[] cells)
- {
- var next = cells.ToArray();
- for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
- {
- for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
- {
- var position = new GridPosition(x, y);
- var cell = cells[level.Index(position)];
- if (cell.Hazards.Smoke < Balancing.Current.SmokeSpreadThreshold)
- continue;
-
- SpreadToNeighbors(level, next, position);
- }
- }
-
- return next;
- }
-
- private static void SpreadToNeighbors(LevelState level, CellState[] next, GridPosition position)
- {
- foreach (var neighbor in position.Neighbors().Where(level.InBounds))
- {
- var neighborCell = next[level.Index(neighbor)];
- if (!neighborCell.IsWalkable || neighborCell.DoorLocked)
- continue;
-
- next[level.Index(neighbor)] = neighborCell with { Hazards = neighborCell.Hazards with { Smoke = Rules.Clamp(neighborCell.Hazards.Smoke + Balancing.Current.SmokeSpreadIncrease) } };
- }
- }
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Effects;
+
+public sealed class SmokeSpreadEffect : IAreaSimulationEffect
+{
+ public CellState[] Apply(LevelState level, CellState[] cells)
+ {
+ var next = cells.ToArray();
+ for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
+ {
+ for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
+ {
+ var position = new GridPosition(x, y);
+ var cell = cells[level.Index(position)];
+ if (cell.Hazards.Smoke < Balancing.Current.SmokeSpreadThreshold)
+ continue;
+
+ SpreadToNeighbors(level, next, position);
+ }
+ }
+
+ return next;
+ }
+
+ private static void SpreadToNeighbors(LevelState level, CellState[] next, GridPosition position)
+ {
+ foreach (var neighbor in position.Neighbors().Where(level.InBounds))
+ {
+ var neighborCell = next[level.Index(neighbor)];
+ if (!neighborCell.IsWalkable || neighborCell.DoorLocked)
+ continue;
+
+ next[level.Index(neighbor)] = neighborCell with { Hazards = neighborCell.Hazards with { Smoke = Rules.Clamp(neighborCell.Hazards.Smoke + Balancing.Current.SmokeSpreadIncrease) } };
+ }
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Hazards/Hazard.cs b/src/ReactorMaintenance.Simulation/Hazards/Hazard.cs
index d6a7814..b8fd677 100644
--- a/src/ReactorMaintenance.Simulation/Hazards/Hazard.cs
+++ b/src/ReactorMaintenance.Simulation/Hazards/Hazard.cs
@@ -1,8 +1,8 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Hazards;
-
-public abstract class Hazard
-{
- public abstract IEnumerable Predict(LevelState level, int turns);
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Hazards;
+
+public abstract class Hazard
+{
+ public abstract IEnumerable Predict(LevelState level, int turns);
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Hazards/IgnitionHazard.cs b/src/ReactorMaintenance.Simulation/Hazards/IgnitionHazard.cs
index 0d320cc..3575e71 100644
--- a/src/ReactorMaintenance.Simulation/Hazards/IgnitionHazard.cs
+++ b/src/ReactorMaintenance.Simulation/Hazards/IgnitionHazard.cs
@@ -1,20 +1,20 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Hazards;
-
-public sealed class IgnitionHazard : Hazard
-{
- public override IEnumerable Predict(LevelState level, int turns)
- {
- for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
- {
- for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
- {
- var position = new GridPosition(x, y);
- var cell = level.GetCell(position);
- if (cell.Hazards.Fire)
- yield return new(EFailureKind.Ignition, position, turns, turns == Balancing.Current.TurnIncrement ? $"FUEL IGNITION PREDICTED AT {x},{y} NEXT TURN" : $"FUEL IGNITION PREDICTED AT {x},{y} IN {turns} TURNS");
- }
- }
- }
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Hazards;
+
+public sealed class IgnitionHazard : Hazard
+{
+ public override IEnumerable Predict(LevelState level, int turns)
+ {
+ for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
+ {
+ for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
+ {
+ var position = new GridPosition(x, y);
+ var cell = level.GetCell(position);
+ if (cell.Hazards.Fire)
+ yield return new(EFailureKind.Ignition, position, turns, turns == Balancing.Current.TurnIncrement ? $"FUEL IGNITION PREDICTED AT {x},{y} NEXT TURN" : $"FUEL IGNITION PREDICTED AT {x},{y} IN {turns} TURNS");
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Hazards/MeltdownHazard.cs b/src/ReactorMaintenance.Simulation/Hazards/MeltdownHazard.cs
index 28e1452..2df3721 100644
--- a/src/ReactorMaintenance.Simulation/Hazards/MeltdownHazard.cs
+++ b/src/ReactorMaintenance.Simulation/Hazards/MeltdownHazard.cs
@@ -1,12 +1,12 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Hazards;
-
-public sealed class MeltdownHazard : Hazard
-{
- public override IEnumerable Predict(LevelState level, int turns)
- {
- if (level.Global is { Lost: true, Status: "CORE MELTDOWN" })
- yield return new(EFailureKind.Meltdown, null, turns, "CORE MELTDOWN APPROACHING");
- }
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Hazards;
+
+public sealed class MeltdownHazard : Hazard
+{
+ public override IEnumerable Predict(LevelState level, int turns)
+ {
+ if (level.Global is { Lost: true, Status: "CORE MELTDOWN" })
+ yield return new(EFailureKind.Meltdown, null, turns, "CORE MELTDOWN APPROACHING");
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Hazards/PipeBurstHazard.cs b/src/ReactorMaintenance.Simulation/Hazards/PipeBurstHazard.cs
index 8c41a9b..cf7d76d 100644
--- a/src/ReactorMaintenance.Simulation/Hazards/PipeBurstHazard.cs
+++ b/src/ReactorMaintenance.Simulation/Hazards/PipeBurstHazard.cs
@@ -1,20 +1,20 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Hazards;
-
-public sealed class PipeBurstHazard : Hazard
-{
- public override IEnumerable Predict(LevelState level, int turns)
- {
- for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
- {
- for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
- {
- var position = new GridPosition(x, y);
- var cell = level.GetCell(position);
- if (cell is { HasPipe: true, PipeOpen: false } && cell.Flow == Balancing.Current.BrokenPipeFlow && cell.LeakRate >= Balancing.Current.BurstLeakRate)
- yield return new(EFailureKind.PipeBurst, position, turns, $"PIPE BURST PREDICTED AT {x},{y} IN {turns} TURNS");
- }
- }
- }
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Hazards;
+
+public sealed class PipeBurstHazard : Hazard
+{
+ public override IEnumerable Predict(LevelState level, int turns)
+ {
+ for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
+ {
+ for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
+ {
+ var position = new GridPosition(x, y);
+ var cell = level.GetCell(position);
+ if (cell is { HasPipe: true, PipeOpen: false } && cell.Flow == Balancing.Current.BrokenPipeFlow && cell.LeakRate >= Balancing.Current.BurstLeakRate)
+ yield return new(EFailureKind.PipeBurst, position, turns, $"PIPE BURST PREDICTED AT {x},{y} IN {turns} TURNS");
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/Hazards/StabilityCollapseHazard.cs b/src/ReactorMaintenance.Simulation/Hazards/StabilityCollapseHazard.cs
index 6510b40..61a97e4 100644
--- a/src/ReactorMaintenance.Simulation/Hazards/StabilityCollapseHazard.cs
+++ b/src/ReactorMaintenance.Simulation/Hazards/StabilityCollapseHazard.cs
@@ -1,12 +1,12 @@
-using ReactorMaintenance.Simulation;
-
-namespace ReactorMaintenance.Simulation.Hazards;
-
-public sealed class StabilityCollapseHazard : Hazard
-{
- public override IEnumerable Predict(LevelState level, int turns)
- {
- if (level.Global is { Lost: true, Status: "FACILITY STABILITY COLLAPSE" })
- yield return new(EFailureKind.StabilityCollapse, null, turns, "FACILITY STABILITY COLLAPSE APPROACHING");
- }
+using ReactorMaintenance.Simulation;
+
+namespace ReactorMaintenance.Simulation.Hazards;
+
+public sealed class StabilityCollapseHazard : Hazard
+{
+ public override IEnumerable Predict(LevelState level, int turns)
+ {
+ if (level.Global is { Lost: true, Status: "FACILITY STABILITY COLLAPSE" })
+ yield return new(EFailureKind.StabilityCollapse, null, turns, "FACILITY STABILITY COLLAPSE APPROACHING");
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/LevelEditor.cs b/src/ReactorMaintenance.Simulation/LevelEditor.cs
index 9daaea9..5d86556 100644
--- a/src/ReactorMaintenance.Simulation/LevelEditor.cs
+++ b/src/ReactorMaintenance.Simulation/LevelEditor.cs
@@ -1,119 +1,119 @@
-namespace ReactorMaintenance.Simulation;
-
-public enum EEditorTool
-{
- Cursor,
- Floor,
- Wall,
- Reactor,
- CoolingPump,
- Generator,
- PressureRegulator,
- DiagnosticTerminal,
- ControlTerminal,
- CoolantPipe,
- FuelPipe,
- PressurePipe,
- Leak,
- Repair,
- Heat,
- Fire,
- Robot
-}
-
-public static class LevelEditor
-{
- public static LevelState Apply(LevelState level, GridPosition position, EEditorTool tool)
- {
- if (!level.InBounds(position))
- return level;
-
- if (tool == EEditorTool.Robot)
- return level.GetCell(position).IsWalkable ? level with { Robot = position } : level;
-
- var cell = level.GetCell(position);
- cell = tool switch {
- EEditorTool.Cursor => cell,
- EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor },
- EEditorTool.Wall => cell with {
- Terrain = ECellTerrain.Wall,
- Prop = ECellProp.None,
- Pipe = EPipeMedium.None,
- Flow = Balancing.Current.MinHazardValue,
- Pressure = Balancing.Current.MinHazardValue,
- LeakRate = Balancing.Current.MinHazardValue,
- PipeOpen = false,
- Powered = false
- },
- EEditorTool.Reactor => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.Reactor },
- EEditorTool.CoolingPump => cell with {
- Terrain = ECellTerrain.Floor,
- Prop = ECellProp.CoolingPump,
- Powered = true
- },
- EEditorTool.Generator => cell with {
- Terrain = ECellTerrain.Floor,
- Prop = ECellProp.Generator,
- Powered = true
- },
- EEditorTool.PressureRegulator => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.PressureRegulator },
- EEditorTool.DiagnosticTerminal => cell with {
- Terrain = ECellTerrain.Floor,
- Prop = ECellProp.DiagnosticTerminal,
- Powered = true
- },
- EEditorTool.ControlTerminal => cell with {
- Terrain = ECellTerrain.Floor,
- Prop = ECellProp.ControlTerminal,
- Powered = true
- },
- EEditorTool.CoolantPipe => cell with {
- Pipe = EPipeMedium.Coolant,
- Flow = Balancing.Current.DefaultPipeFlow,
- Pressure = Balancing.Current.DefaultPipePressure,
- Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
- PipeOpen = true
- },
- EEditorTool.FuelPipe => cell with {
- Pipe = EPipeMedium.Fuel,
- Flow = Balancing.Current.DefaultPipeFlow,
- Pressure = Balancing.Current.DefaultPipePressure,
- Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
- PipeOpen = true
- },
- EEditorTool.PressurePipe => cell with {
- Pipe = EPipeMedium.Pressure,
- Flow = Balancing.Current.DefaultPressurePipeFlow,
- Pressure = Balancing.Current.DefaultPressurePipePressure,
- Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
- PipeOpen = true
- },
- EEditorTool.Leak => cell with {
- LeakRate = Math.Max(Balancing.Current.MinimumLeakRate, cell.LeakRate),
- Integrity = Math.Min(cell.Integrity, Balancing.Current.DamagedPipeIntegrity)
- },
- EEditorTool.Repair => cell with {
- LeakRate = Balancing.Current.RepairedLeakRate,
- Integrity = Balancing.Current.DefaultCellIntegrity,
- Hazards = cell.Hazards with {
- Fire = false,
- ElectricalCharge = Balancing.Current.RepairedElectricalCharge
- }
- },
- EEditorTool.Heat => cell with { Hazards = cell.Hazards with { Heat = Rules.Clamp(cell.Hazards.Heat + Balancing.Current.HeatToolIncrease) } },
- EEditorTool.Fire => cell with {
- Hazards = cell.Hazards with {
- Fire = !cell.Hazards.Fire,
- Heat = Math.Max(cell.Hazards.Heat, Balancing.Current.FireToolMinimumHeat),
- Smoke = Math.Max(cell.Hazards.Smoke, Balancing.Current.FireToolMinimumSmoke)
- }
- },
- _ => cell
- };
-
- if (cell.Terrain == ECellTerrain.Wall)
- cell = cell with { Hazards = new() };
-
- return level.SetCell(position, cell);
- }
+namespace ReactorMaintenance.Simulation;
+
+public enum EEditorTool
+{
+ Cursor,
+ Floor,
+ Wall,
+ Reactor,
+ CoolingPump,
+ Generator,
+ PressureRegulator,
+ DiagnosticTerminal,
+ ControlTerminal,
+ CoolantPipe,
+ FuelPipe,
+ PressurePipe,
+ Leak,
+ Repair,
+ Heat,
+ Fire,
+ Robot
+}
+
+public static class LevelEditor
+{
+ public static LevelState Apply(LevelState level, GridPosition position, EEditorTool tool)
+ {
+ if (!level.InBounds(position))
+ return level;
+
+ if (tool == EEditorTool.Robot)
+ return level.GetCell(position).IsWalkable ? level with { Robot = position } : level;
+
+ var cell = level.GetCell(position);
+ cell = tool switch {
+ EEditorTool.Cursor => cell,
+ EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor },
+ EEditorTool.Wall => cell with {
+ Terrain = ECellTerrain.Wall,
+ Prop = ECellProp.None,
+ Pipe = EPipeMedium.None,
+ Flow = Balancing.Current.MinHazardValue,
+ Pressure = Balancing.Current.MinHazardValue,
+ LeakRate = Balancing.Current.MinHazardValue,
+ PipeOpen = false,
+ Powered = false
+ },
+ EEditorTool.Reactor => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.Reactor },
+ EEditorTool.CoolingPump => cell with {
+ Terrain = ECellTerrain.Floor,
+ Prop = ECellProp.CoolingPump,
+ Powered = true
+ },
+ EEditorTool.Generator => cell with {
+ Terrain = ECellTerrain.Floor,
+ Prop = ECellProp.Generator,
+ Powered = true
+ },
+ EEditorTool.PressureRegulator => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.PressureRegulator },
+ EEditorTool.DiagnosticTerminal => cell with {
+ Terrain = ECellTerrain.Floor,
+ Prop = ECellProp.DiagnosticTerminal,
+ Powered = true
+ },
+ EEditorTool.ControlTerminal => cell with {
+ Terrain = ECellTerrain.Floor,
+ Prop = ECellProp.ControlTerminal,
+ Powered = true
+ },
+ EEditorTool.CoolantPipe => cell with {
+ Pipe = EPipeMedium.Coolant,
+ Flow = Balancing.Current.DefaultPipeFlow,
+ Pressure = Balancing.Current.DefaultPipePressure,
+ Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
+ PipeOpen = true
+ },
+ EEditorTool.FuelPipe => cell with {
+ Pipe = EPipeMedium.Fuel,
+ Flow = Balancing.Current.DefaultPipeFlow,
+ Pressure = Balancing.Current.DefaultPipePressure,
+ Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
+ PipeOpen = true
+ },
+ EEditorTool.PressurePipe => cell with {
+ Pipe = EPipeMedium.Pressure,
+ Flow = Balancing.Current.DefaultPressurePipeFlow,
+ Pressure = Balancing.Current.DefaultPressurePipePressure,
+ Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
+ PipeOpen = true
+ },
+ EEditorTool.Leak => cell with {
+ LeakRate = Math.Max(Balancing.Current.MinimumLeakRate, cell.LeakRate),
+ Integrity = Math.Min(cell.Integrity, Balancing.Current.DamagedPipeIntegrity)
+ },
+ EEditorTool.Repair => cell with {
+ LeakRate = Balancing.Current.RepairedLeakRate,
+ Integrity = Balancing.Current.DefaultCellIntegrity,
+ Hazards = cell.Hazards with {
+ Fire = false,
+ ElectricalCharge = Balancing.Current.RepairedElectricalCharge
+ }
+ },
+ EEditorTool.Heat => cell with { Hazards = cell.Hazards with { Heat = Rules.Clamp(cell.Hazards.Heat + Balancing.Current.HeatToolIncrease) } },
+ EEditorTool.Fire => cell with {
+ Hazards = cell.Hazards with {
+ Fire = !cell.Hazards.Fire,
+ Heat = Math.Max(cell.Hazards.Heat, Balancing.Current.FireToolMinimumHeat),
+ Smoke = Math.Max(cell.Hazards.Smoke, Balancing.Current.FireToolMinimumSmoke)
+ }
+ },
+ _ => cell
+ };
+
+ if (cell.Terrain == ECellTerrain.Wall)
+ cell = cell with { Hazards = new() };
+
+ return level.SetCell(position, cell);
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/LevelSerializer.cs b/src/ReactorMaintenance.Simulation/LevelSerializer.cs
index 99b8fb4..95b6e33 100644
--- a/src/ReactorMaintenance.Simulation/LevelSerializer.cs
+++ b/src/ReactorMaintenance.Simulation/LevelSerializer.cs
@@ -1,39 +1,39 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace ReactorMaintenance.Simulation;
-
-public static class LevelSerializer
-{
- private const int c_CurrentVersion = 1;
-
- public static string Serialize(LevelState level)
- {
- return JsonSerializer.Serialize(new LevelFile {
- Version = c_CurrentVersion,
- Level = level
- }, Options);
- }
-
- public static LevelState Deserialize(string json)
- {
- var file = JsonSerializer.Deserialize(json, Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
- var level = file.Version switch {
- c_CurrentVersion => file.Level,
- _ => throw new InvalidOperationException($"Unsupported level file version {file.Version}.")
- };
-
- return level.Cells.Length != level.Width * level.Height ? throw new InvalidOperationException("Level cell count does not match its dimensions.") : level;
- }
-
- private static readonly JsonSerializerOptions Options = new() {
- WriteIndented = true,
- Converters = { new JsonStringEnumConverter() }
- };
-
- private sealed record LevelFile
- {
- public int Version { get; init; }
- public LevelState Level { get; init; } = new();
- }
-}
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace ReactorMaintenance.Simulation;
+
+public static class LevelSerializer
+{
+ private const int c_CurrentVersion = 1;
+
+ public static string Serialize(LevelState level)
+ {
+ return JsonSerializer.Serialize(new LevelFile {
+ Version = c_CurrentVersion,
+ Level = level
+ }, Options);
+ }
+
+ public static LevelState Deserialize(string json)
+ {
+ var file = JsonSerializer.Deserialize(json, Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
+ var level = file.Version switch {
+ c_CurrentVersion => file.Level,
+ _ => throw new InvalidOperationException($"Unsupported level file version {file.Version}.")
+ };
+
+ return level.Cells.Length != level.Width * level.Height ? throw new InvalidOperationException("Level cell count does not match its dimensions.") : level;
+ }
+
+ private static readonly JsonSerializerOptions Options = new() {
+ WriteIndented = true,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ private sealed record LevelFile
+ {
+ public int Version { get; init; }
+ public LevelState Level { get; init; } = new();
+ }
+}
diff --git a/src/ReactorMaintenance.Simulation/Models.cs b/src/ReactorMaintenance.Simulation/Models.cs
index 63ef131..cfe0289 100644
--- a/src/ReactorMaintenance.Simulation/Models.cs
+++ b/src/ReactorMaintenance.Simulation/Models.cs
@@ -1,181 +1,181 @@
-namespace ReactorMaintenance.Simulation;
-
-public enum ECellTerrain
-{
- Floor,
- Wall
-}
-
-public enum ECellProp
-{
- None,
- Reactor,
- CoolingPump,
- Generator,
- PressureRegulator,
- DiagnosticTerminal,
- ControlTerminal
-}
-
-public enum EPipeMedium
-{
- None,
- Pressure,
- Coolant,
- Fuel
-}
-
-public enum EFailureKind
-{
- PipeBurst,
- Ignition,
- Meltdown,
- StabilityCollapse,
- ReactorReady
-}
-
-public sealed record GridPosition(int X, int Y)
-{
- public IEnumerable Neighbors()
- {
- yield return new(X - Balancing.Current.NeighborDistance, Y);
- yield return new(X + Balancing.Current.NeighborDistance, Y);
- yield return new(X, Y - Balancing.Current.NeighborDistance);
- yield return new(X, Y + Balancing.Current.NeighborDistance);
- }
-}
-
-public sealed record HazardState
-{
- public HazardState Clamp()
- {
- return this with {
- Heat = Rules.Clamp(Heat),
- Smoke = Rules.Clamp(Smoke),
- FuelVapor = Rules.Clamp(FuelVapor),
- LiquidFuel = Rules.Clamp(LiquidFuel),
- CoolantPooling = Rules.Clamp(CoolantPooling),
- ElectricalCharge = Rules.Clamp(ElectricalCharge),
- Stability = Rules.Clamp(Stability)
- };
- }
-
- public int Heat { get; init; }
- public int Smoke { get; init; }
- public int FuelVapor { get; init; }
- public int LiquidFuel { get; init; }
- public int CoolantPooling { get; init; }
- public int ElectricalCharge { get; init; }
- public int Stability { get; init; } = Balancing.Current.DefaultHazardStability;
- public bool Fire { get; init; }
-}
-
-public sealed record CellState
-{
- public ECellTerrain Terrain { get; init; } = ECellTerrain.Floor;
- public ECellProp Prop { get; init; }
- public EPipeMedium Pipe { get; init; }
- public int Flow { get; init; }
- public int Pressure { get; init; }
- public int Integrity { get; init; } = Balancing.Current.DefaultCellIntegrity;
- public int LeakRate { get; init; }
- public bool PipeOpen { get; init; } = true;
- public bool Powered { get; init; }
- public bool DoorLocked { get; init; }
- public HazardState Hazards { get; init; } = new();
- public bool IsWalkable => Terrain != ECellTerrain.Wall;
- public bool HasPipe => Pipe != EPipeMedium.None;
-}
-
-public sealed record GlobalState
-{
- public int Turn { get; init; }
- public int ActionsPerTurn { get; init; } = Balancing.Current.DefaultActionsPerTurn;
- public int CoreHeat { get; init; } = Balancing.Current.DefaultCoreHeat;
- public int FacilityStability { get; init; } = Balancing.Current.DefaultFacilityStability;
- public int Power { get; init; } = Balancing.Current.DefaultPower;
- public int Cooling { get; init; } = Balancing.Current.DefaultCooling;
- public bool ReactorActivated { get; init; }
- public bool Lost { get; init; }
- public string Status { get; init; } = "STABILIZE SYSTEMS";
-}
-
-public sealed record Forecast(EFailureKind Kind, GridPosition? Position, int Turns, string Message);
-
-public sealed record LevelState
-{
- public static LevelState Create(string name, int width, int height)
- {
- if (width < Balancing.Current.MinimumLevelSize || height < Balancing.Current.MinimumLevelSize)
- throw new ArgumentOutOfRangeException(nameof(width), $"Levels must be at least {Balancing.Current.MinimumLevelSize}x{Balancing.Current.MinimumLevelSize}.");
-
- var cells = CreateCells(width, height);
- for (var y = Balancing.Current.FirstGridCoordinate; y < height; y++)
- {
- for (var x = Balancing.Current.FirstGridCoordinate; x < width; x++)
- {
- if (x == Balancing.Current.FirstGridCoordinate || y == Balancing.Current.FirstGridCoordinate || x == width - Balancing.Current.NeighborDistance || y == height - Balancing.Current.NeighborDistance)
- cells[y * width + x] = cells[y * width + x] with { Terrain = ECellTerrain.Wall };
- }
- }
-
- return new() {
- Name = name,
- Width = width,
- Height = height,
- Cells = cells,
- Robot = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate)
- };
- }
-
- public CellState GetCell(GridPosition position)
- {
- EnsureInBounds(position);
- return Cells[Index(position)];
- }
-
- public LevelState SetCell(GridPosition position, CellState cell)
- {
- EnsureInBounds(position);
- var cells = Cells.ToArray();
- cells[Index(position)] = cell;
- return this with { Cells = cells };
- }
-
- public bool InBounds(GridPosition position)
- {
- return position.X >= Balancing.Current.FirstGridCoordinate && position.Y >= Balancing.Current.FirstGridCoordinate && position.X < Width && position.Y < Height;
- }
-
- public int Index(GridPosition position)
- {
- return position.Y * Width + position.X;
- }
-
- private void EnsureInBounds(GridPosition position)
- {
- if (!InBounds(position))
- throw new ArgumentOutOfRangeException(nameof(position), $"Position {position.X},{position.Y} is outside {Width}x{Height}.");
- }
-
- private static CellState[] CreateCells(int width, int height)
- {
- return Enumerable.Range(Balancing.Current.FirstGridCoordinate, width * height).Select(_ => new CellState()).ToArray();
- }
-
- public string Name { get; init; } = "New Reactor";
- public int Width { get; init; } = Balancing.Current.DefaultLevelWidth;
- public int Height { get; init; } = Balancing.Current.DefaultLevelHeight;
- public CellState[] Cells { get; init; } = CreateCells(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
- public GridPosition Robot { get; init; } = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate);
- public GlobalState Global { get; init; } = new();
- public IReadOnlyList Forecasts { get; init; } = Array.Empty();
-}
-
-internal static class Rules
-{
- public static int Clamp(int value)
- {
- return Math.Clamp(value, Balancing.Current.MinHazardValue, Balancing.Current.MaxHazardValue);
- }
+namespace ReactorMaintenance.Simulation;
+
+public enum ECellTerrain
+{
+ Floor,
+ Wall
+}
+
+public enum ECellProp
+{
+ None,
+ Reactor,
+ CoolingPump,
+ Generator,
+ PressureRegulator,
+ DiagnosticTerminal,
+ ControlTerminal
+}
+
+public enum EPipeMedium
+{
+ None,
+ Pressure,
+ Coolant,
+ Fuel
+}
+
+public enum EFailureKind
+{
+ PipeBurst,
+ Ignition,
+ Meltdown,
+ StabilityCollapse,
+ ReactorReady
+}
+
+public sealed record GridPosition(int X, int Y)
+{
+ public IEnumerable Neighbors()
+ {
+ yield return new(X - Balancing.Current.NeighborDistance, Y);
+ yield return new(X + Balancing.Current.NeighborDistance, Y);
+ yield return new(X, Y - Balancing.Current.NeighborDistance);
+ yield return new(X, Y + Balancing.Current.NeighborDistance);
+ }
+}
+
+public sealed record HazardState
+{
+ public HazardState Clamp()
+ {
+ return this with {
+ Heat = Rules.Clamp(Heat),
+ Smoke = Rules.Clamp(Smoke),
+ FuelVapor = Rules.Clamp(FuelVapor),
+ LiquidFuel = Rules.Clamp(LiquidFuel),
+ CoolantPooling = Rules.Clamp(CoolantPooling),
+ ElectricalCharge = Rules.Clamp(ElectricalCharge),
+ Stability = Rules.Clamp(Stability)
+ };
+ }
+
+ public int Heat { get; init; }
+ public int Smoke { get; init; }
+ public int FuelVapor { get; init; }
+ public int LiquidFuel { get; init; }
+ public int CoolantPooling { get; init; }
+ public int ElectricalCharge { get; init; }
+ public int Stability { get; init; } = Balancing.Current.DefaultHazardStability;
+ public bool Fire { get; init; }
+}
+
+public sealed record CellState
+{
+ public ECellTerrain Terrain { get; init; } = ECellTerrain.Floor;
+ public ECellProp Prop { get; init; }
+ public EPipeMedium Pipe { get; init; }
+ public int Flow { get; init; }
+ public int Pressure { get; init; }
+ public int Integrity { get; init; } = Balancing.Current.DefaultCellIntegrity;
+ public int LeakRate { get; init; }
+ public bool PipeOpen { get; init; } = true;
+ public bool Powered { get; init; }
+ public bool DoorLocked { get; init; }
+ public HazardState Hazards { get; init; } = new();
+ public bool IsWalkable => Terrain != ECellTerrain.Wall;
+ public bool HasPipe => Pipe != EPipeMedium.None;
+}
+
+public sealed record GlobalState
+{
+ public int Turn { get; init; }
+ public int ActionsPerTurn { get; init; } = Balancing.Current.DefaultActionsPerTurn;
+ public int CoreHeat { get; init; } = Balancing.Current.DefaultCoreHeat;
+ public int FacilityStability { get; init; } = Balancing.Current.DefaultFacilityStability;
+ public int Power { get; init; } = Balancing.Current.DefaultPower;
+ public int Cooling { get; init; } = Balancing.Current.DefaultCooling;
+ public bool ReactorActivated { get; init; }
+ public bool Lost { get; init; }
+ public string Status { get; init; } = "STABILIZE SYSTEMS";
+}
+
+public sealed record Forecast(EFailureKind Kind, GridPosition? Position, int Turns, string Message);
+
+public sealed record LevelState
+{
+ public static LevelState Create(string name, int width, int height)
+ {
+ if (width < Balancing.Current.MinimumLevelSize || height < Balancing.Current.MinimumLevelSize)
+ throw new ArgumentOutOfRangeException(nameof(width), $"Levels must be at least {Balancing.Current.MinimumLevelSize}x{Balancing.Current.MinimumLevelSize}.");
+
+ var cells = CreateCells(width, height);
+ for (var y = Balancing.Current.FirstGridCoordinate; y < height; y++)
+ {
+ for (var x = Balancing.Current.FirstGridCoordinate; x < width; x++)
+ {
+ if (x == Balancing.Current.FirstGridCoordinate || y == Balancing.Current.FirstGridCoordinate || x == width - Balancing.Current.NeighborDistance || y == height - Balancing.Current.NeighborDistance)
+ cells[y * width + x] = cells[y * width + x] with { Terrain = ECellTerrain.Wall };
+ }
+ }
+
+ return new() {
+ Name = name,
+ Width = width,
+ Height = height,
+ Cells = cells,
+ Robot = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate)
+ };
+ }
+
+ public CellState GetCell(GridPosition position)
+ {
+ EnsureInBounds(position);
+ return Cells[Index(position)];
+ }
+
+ public LevelState SetCell(GridPosition position, CellState cell)
+ {
+ EnsureInBounds(position);
+ var cells = Cells.ToArray();
+ cells[Index(position)] = cell;
+ return this with { Cells = cells };
+ }
+
+ public bool InBounds(GridPosition position)
+ {
+ return position.X >= Balancing.Current.FirstGridCoordinate && position.Y >= Balancing.Current.FirstGridCoordinate && position.X < Width && position.Y < Height;
+ }
+
+ public int Index(GridPosition position)
+ {
+ return position.Y * Width + position.X;
+ }
+
+ private void EnsureInBounds(GridPosition position)
+ {
+ if (!InBounds(position))
+ throw new ArgumentOutOfRangeException(nameof(position), $"Position {position.X},{position.Y} is outside {Width}x{Height}.");
+ }
+
+ private static CellState[] CreateCells(int width, int height)
+ {
+ return Enumerable.Range(Balancing.Current.FirstGridCoordinate, width * height).Select(_ => new CellState()).ToArray();
+ }
+
+ public string Name { get; init; } = "New Reactor";
+ public int Width { get; init; } = Balancing.Current.DefaultLevelWidth;
+ public int Height { get; init; } = Balancing.Current.DefaultLevelHeight;
+ public CellState[] Cells { get; init; } = CreateCells(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
+ public GridPosition Robot { get; init; } = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate);
+ public GlobalState Global { get; init; } = new();
+ public IReadOnlyList Forecasts { get; init; } = Array.Empty();
+}
+
+internal static class Rules
+{
+ public static int Clamp(int value)
+ {
+ return Math.Clamp(value, Balancing.Current.MinHazardValue, Balancing.Current.MaxHazardValue);
+ }
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj b/src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj
index fa71b7a..bb23fb7 100644
--- a/src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj
+++ b/src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj
@@ -1,9 +1,9 @@
-
-
-
- net8.0
- enable
- enable
-
-
-
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/src/ReactorMaintenance.Simulation/SimulationEngine.cs b/src/ReactorMaintenance.Simulation/SimulationEngine.cs
index d23eae9..e113065 100644
--- a/src/ReactorMaintenance.Simulation/SimulationEngine.cs
+++ b/src/ReactorMaintenance.Simulation/SimulationEngine.cs
@@ -1,150 +1,150 @@
-using ReactorMaintenance.Simulation.Effects;
-using ReactorMaintenance.Simulation.Hazards;
-
-namespace ReactorMaintenance.Simulation;
-
-public sealed class SimulationEngine(IEnumerable effects, IEnumerable areaEffects, IEnumerable hazards)
-{
- private sealed record ForecastKey(EFailureKind Kind, GridPosition? Position);
-
- public SimulationEngine()
- : this(
- [new PipeLeakEffect(), new MachineEffect(), new FireAndElectricalHazardEffect(), new CellIntegrityEffect()],
- [new SmokeSpreadEffect()],
- [new PipeBurstHazard(), new IgnitionHazard(), new MeltdownHazard(), new StabilityCollapseHazard()])
- {
- }
-
- public LevelState AdvanceTurn(LevelState level)
- {
- return AdvanceTurn(level, true);
- }
-
- public IReadOnlyList Forecast(LevelState level)
- {
- var forecasts = new List();
- var seen = new HashSet();
- var forecastLevel = level with { Cells = level.Cells.ToArray(), Forecasts = Array.Empty() };
- if (forecastLevel.Global.Lost)
- AddHazardForecasts(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
-
- AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
-
- if (IsReactorReady(forecastLevel) || forecastLevel.Global.Lost || forecastLevel.Global.ReactorActivated)
- return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
-
- for (var step = Balancing.Current.TurnIncrement; step <= Balancing.Current.MaxForecastStepCount; step++)
- {
- forecastLevel = AdvanceTurn(forecastLevel, false);
- AddHazardForecasts(forecasts, seen, forecastLevel, step);
- AddReactorReadyForecast(forecasts, seen, forecastLevel, step);
-
- if (forecastLevel.Global.Lost || IsReactorReady(forecastLevel) || forecastLevel.Global.ReactorActivated)
- break;
- }
-
- return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
- }
-
- public LevelState ActivateReactor(LevelState level)
- {
- if (!IsReactorReady(level))
- return level with { Global = level.Global with { Status = "REACTOR NOT READY" } };
-
- return level with {
- Global = level.Global with {
- ReactorActivated = true,
- Status = "REACTOR ONLINE"
- }
- };
- }
-
- private LevelState AdvanceTurn(LevelState level, bool updateForecasts)
- {
- var cells = level.Cells.ToArray();
-
- for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
- {
- for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
- {
- var position = new GridPosition(x, y);
- var index = level.Index(position);
- var cell = cells[index];
-
- if (!cell.IsWalkable)
- continue;
-
- foreach (var effect in m_Effects)
- cell = effect.Apply(cell);
-
- cells[index] = cell with { Hazards = cell.Hazards.Clamp() };
- }
- }
-
- foreach (var areaEffect in m_AreaEffects)
- cells = areaEffect.Apply(level, cells);
-
- var global = UpdateGlobal(level, cells);
- var next = level with {
- Cells = cells,
- Global = global with { Turn = level.Global.Turn + Balancing.Current.TurnIncrement }
- };
-
- return updateForecasts ? next with { Forecasts = Forecast(next) } : next;
- }
-
- private void AddHazardForecasts(List forecasts, HashSet seen, LevelState level, int turns)
- {
- foreach (var hazard in m_Hazards)
- {
- foreach (var forecast in hazard.Predict(level, turns))
- AddForecast(forecasts, seen, forecast);
- }
- }
-
- private static void AddReactorReadyForecast(List forecasts, HashSet seen, LevelState level, int turns)
- {
- if (IsReactorReady(level))
- AddForecast(forecasts, seen, new(EFailureKind.ReactorReady, null, turns, "REACTOR READY"));
- }
-
- private static void AddForecast(List forecasts, HashSet seen, Forecast forecast)
- {
- if (seen.Add(new(forecast.Kind, forecast.Position)))
- forecasts.Add(forecast);
- }
-
- private static GlobalState UpdateGlobal(LevelState level, CellState[] cells)
- {
- var reactorHeat = cells.Where(c => c.Prop == ECellProp.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max();
- var poweredGenerators = cells.Count(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
- var poweredPumps = cells.Count(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
- var damagedCriticalCells = cells.Count(c => c.Prop is ECellProp.Reactor or ECellProp.Generator or ECellProp.CoolingPump && c.Hazards.Stability <= Balancing.Current.CriticalCellStabilityThreshold);
- var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells);
- var lost = reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold || stability <= Balancing.Current.StabilityCollapseThreshold;
- var status = lost ? reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS";
- var global = level.Global with {
- CoreHeat = Rules.Clamp(reactorHeat - poweredPumps),
- Power = Rules.Clamp(poweredGenerators * Balancing.Current.GeneratorPowerOutput),
- Cooling = Rules.Clamp(poweredPumps * Balancing.Current.CoolingPumpOutput),
- FacilityStability = stability,
- Lost = lost,
- Status = status
- };
-
- return IsReactorReady(level with { Cells = cells, Global = global }) ? global with { Status = "REACTOR READY" } : global;
- }
-
- private static bool IsReactorReady(LevelState level)
- {
- var hasReactor = level.Cells.Any(c => c.Prop == ECellProp.Reactor);
- var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
- var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
- var reactorStable = level.Global.CoreHeat < Balancing.Current.ReactorReadyCoreHeatThreshold;
- return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost;
- }
-
- private readonly IReadOnlyList m_AreaEffects = areaEffects.ToArray();
- private readonly IReadOnlyList m_Effects = effects.ToArray();
- private readonly IReadOnlyList m_Hazards = hazards.ToArray();
+using ReactorMaintenance.Simulation.Effects;
+using ReactorMaintenance.Simulation.Hazards;
+
+namespace ReactorMaintenance.Simulation;
+
+public sealed class SimulationEngine(IEnumerable effects, IEnumerable areaEffects, IEnumerable hazards)
+{
+ private sealed record ForecastKey(EFailureKind Kind, GridPosition? Position);
+
+ public SimulationEngine()
+ : this(
+ [new PipeLeakEffect(), new MachineEffect(), new FireAndElectricalHazardEffect(), new CellIntegrityEffect()],
+ [new SmokeSpreadEffect()],
+ [new PipeBurstHazard(), new IgnitionHazard(), new MeltdownHazard(), new StabilityCollapseHazard()])
+ {
+ }
+
+ public LevelState AdvanceTurn(LevelState level)
+ {
+ return AdvanceTurn(level, true);
+ }
+
+ public IReadOnlyList Forecast(LevelState level)
+ {
+ var forecasts = new List();
+ var seen = new HashSet();
+ var forecastLevel = level with { Cells = level.Cells.ToArray(), Forecasts = Array.Empty() };
+ if (forecastLevel.Global.Lost)
+ AddHazardForecasts(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
+
+ AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
+
+ if (IsReactorReady(forecastLevel) || forecastLevel.Global.Lost || forecastLevel.Global.ReactorActivated)
+ return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
+
+ for (var step = Balancing.Current.TurnIncrement; step <= Balancing.Current.MaxForecastStepCount; step++)
+ {
+ forecastLevel = AdvanceTurn(forecastLevel, false);
+ AddHazardForecasts(forecasts, seen, forecastLevel, step);
+ AddReactorReadyForecast(forecasts, seen, forecastLevel, step);
+
+ if (forecastLevel.Global.Lost || IsReactorReady(forecastLevel) || forecastLevel.Global.ReactorActivated)
+ break;
+ }
+
+ return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
+ }
+
+ public LevelState ActivateReactor(LevelState level)
+ {
+ if (!IsReactorReady(level))
+ return level with { Global = level.Global with { Status = "REACTOR NOT READY" } };
+
+ return level with {
+ Global = level.Global with {
+ ReactorActivated = true,
+ Status = "REACTOR ONLINE"
+ }
+ };
+ }
+
+ private LevelState AdvanceTurn(LevelState level, bool updateForecasts)
+ {
+ var cells = level.Cells.ToArray();
+
+ for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
+ {
+ for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
+ {
+ var position = new GridPosition(x, y);
+ var index = level.Index(position);
+ var cell = cells[index];
+
+ if (!cell.IsWalkable)
+ continue;
+
+ foreach (var effect in m_Effects)
+ cell = effect.Apply(cell);
+
+ cells[index] = cell with { Hazards = cell.Hazards.Clamp() };
+ }
+ }
+
+ foreach (var areaEffect in m_AreaEffects)
+ cells = areaEffect.Apply(level, cells);
+
+ var global = UpdateGlobal(level, cells);
+ var next = level with {
+ Cells = cells,
+ Global = global with { Turn = level.Global.Turn + Balancing.Current.TurnIncrement }
+ };
+
+ return updateForecasts ? next with { Forecasts = Forecast(next) } : next;
+ }
+
+ private void AddHazardForecasts(List forecasts, HashSet seen, LevelState level, int turns)
+ {
+ foreach (var hazard in m_Hazards)
+ {
+ foreach (var forecast in hazard.Predict(level, turns))
+ AddForecast(forecasts, seen, forecast);
+ }
+ }
+
+ private static void AddReactorReadyForecast(List forecasts, HashSet seen, LevelState level, int turns)
+ {
+ if (IsReactorReady(level))
+ AddForecast(forecasts, seen, new(EFailureKind.ReactorReady, null, turns, "REACTOR READY"));
+ }
+
+ private static void AddForecast(List forecasts, HashSet seen, Forecast forecast)
+ {
+ if (seen.Add(new(forecast.Kind, forecast.Position)))
+ forecasts.Add(forecast);
+ }
+
+ private static GlobalState UpdateGlobal(LevelState level, CellState[] cells)
+ {
+ var reactorHeat = cells.Where(c => c.Prop == ECellProp.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max();
+ var poweredGenerators = cells.Count(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
+ var poweredPumps = cells.Count(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
+ var damagedCriticalCells = cells.Count(c => c.Prop is ECellProp.Reactor or ECellProp.Generator or ECellProp.CoolingPump && c.Hazards.Stability <= Balancing.Current.CriticalCellStabilityThreshold);
+ var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells);
+ var lost = reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold || stability <= Balancing.Current.StabilityCollapseThreshold;
+ var status = lost ? reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS";
+ var global = level.Global with {
+ CoreHeat = Rules.Clamp(reactorHeat - poweredPumps),
+ Power = Rules.Clamp(poweredGenerators * Balancing.Current.GeneratorPowerOutput),
+ Cooling = Rules.Clamp(poweredPumps * Balancing.Current.CoolingPumpOutput),
+ FacilityStability = stability,
+ Lost = lost,
+ Status = status
+ };
+
+ return IsReactorReady(level with { Cells = cells, Global = global }) ? global with { Status = "REACTOR READY" } : global;
+ }
+
+ private static bool IsReactorReady(LevelState level)
+ {
+ var hasReactor = level.Cells.Any(c => c.Prop == ECellProp.Reactor);
+ var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
+ var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
+ var reactorStable = level.Global.CoreHeat < Balancing.Current.ReactorReadyCoreHeatThreshold;
+ return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost;
+ }
+
+ private readonly IReadOnlyList m_AreaEffects = areaEffects.ToArray();
+ private readonly IReadOnlyList m_Effects = effects.ToArray();
+ private readonly IReadOnlyList m_Hazards = hazards.ToArray();
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Win2D/App.xaml b/src/ReactorMaintenance.Win2D/App.xaml
index 2ee7cdd..410f7ee 100644
--- a/src/ReactorMaintenance.Win2D/App.xaml
+++ b/src/ReactorMaintenance.Win2D/App.xaml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Win2D/App.xaml.cs b/src/ReactorMaintenance.Win2D/App.xaml.cs
index 3bb964e..631d641 100644
--- a/src/ReactorMaintenance.Win2D/App.xaml.cs
+++ b/src/ReactorMaintenance.Win2D/App.xaml.cs
@@ -1,19 +1,19 @@
-using Microsoft.UI.Xaml;
-
-namespace ReactorMaintenance.Win2D;
-
-public partial class App
-{
- public App()
- {
- InitializeComponent();
- }
-
- protected override void OnLaunched(LaunchActivatedEventArgs args)
- {
- m_Window = new MainWindow();
- m_Window.Activate();
- }
-
- private Window? m_Window;
+using Microsoft.UI.Xaml;
+
+namespace ReactorMaintenance.Win2D;
+
+public partial class App
+{
+ public App()
+ {
+ InitializeComponent();
+ }
+
+ protected override void OnLaunched(LaunchActivatedEventArgs args)
+ {
+ m_Window = new MainWindow();
+ m_Window.Activate();
+ }
+
+ private Window? m_Window;
}
\ No newline at end of file
diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml b/src/ReactorMaintenance.Win2D/MainWindow.xaml
index 4868552..5cf8599 100644
--- a/src/ReactorMaintenance.Win2D/MainWindow.xaml
+++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml
@@ -1,115 +1,115 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs
index 9d86e0c..a179563 100644
--- a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs
+++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs
@@ -1,759 +1,729 @@
-using Microsoft.Graphics.Canvas;
-using Microsoft.Graphics.Canvas.UI;
-using Microsoft.Graphics.Canvas.UI.Xaml;
-using Microsoft.UI;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Input;
-using Microsoft.UI.Xaml.Media.Imaging;
-using ReactorMaintenance.Simulation;
-using System.Globalization;
-using Windows.Foundation;
-using Windows.Storage;
-using Windows.Storage.Pickers;
-using Windows.UI;
-using Windows.UI.Popups;
-using WinRT.Interop;
-
-namespace ReactorMaintenance.Win2D;
-
-public sealed partial class MainWindow
-{
- private sealed record CanvasLayout(double CellSize, double OriginX, double OriginY)
- {
- public Rect CellRect(int x, int y)
- {
- return new(OriginX + (x * CellSize), OriginY + (y * CellSize), CellSize, CellSize);
- }
-
- public Rect DualTileRect(int x, int y)
- {
- return new(OriginX + ((x - 0.5) * CellSize), OriginY + ((y - 0.5) * CellSize), CellSize, CellSize);
- }
- }
-
- private sealed record ForecastViewModel(BitmapImage Icon, string Message);
-
- private sealed class EditorToolViewModel(EEditorTool tool, BitmapImage? icon, string label)
- {
- public EEditorTool Tool { get; } = tool;
- public BitmapImage? Icon { get; } = icon;
- public string Label { get; } = label;
- public bool IsSelected { get; set; }
- }
-
- public MainWindow()
- {
- InitializeComponent();
-
- m_Level = BuildStarterLevel();
- m_EditorTools = Enum.GetValues().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString()) { IsSelected = tool == m_SelectedTool }).ToArray();
- ToolPicker.ItemsSource = m_EditorTools;
- RefreshInspector();
- }
-
- private void LevelCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
- {
- args.TrackAsyncAction(LoadImagesAsync(sender).AsAsyncAction());
- }
-
- private async Task LoadImagesAsync(CanvasControl sender)
- {
- m_TerrainTilemap = await LoadCanvasBitmapAsync(sender, "Images", "tilemap.png");
- m_RobotSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "robot.png");
- m_LeakSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "leak.png");
- m_HeatSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "heat.png");
- m_FireSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "fire.png");
-
- m_PropSprites[ECellProp.Reactor] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "reactor.png");
- m_PropSprites[ECellProp.CoolingPump] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "cooling-pump.png");
- m_PropSprites[ECellProp.Generator] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "generator.png");
- m_PropSprites[ECellProp.PressureRegulator] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "pressure-regulator.png");
- m_PropSprites[ECellProp.DiagnosticTerminal] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "diagnostic-terminal.png");
- m_PropSprites[ECellProp.ControlTerminal] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "control-terminal.png");
-
- m_PipeTilemaps[EPipeMedium.Pressure] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-pressure-tilemap.png");
- m_PipeTilemaps[EPipeMedium.Coolant] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-coolant-tilemap.png");
- m_PipeTilemaps[EPipeMedium.Fuel] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-fuel-tilemap.png");
- }
-
- private static async Task LoadCanvasBitmapAsync(CanvasControl sender, params string[] pathParts)
- {
- var path = Path.Combine([AppContext.BaseDirectory, .. pathParts]);
- return await CanvasBitmap.LoadAsync(sender, path);
- }
-
- private void ToolToggle_Checked(object sender, RoutedEventArgs e)
- {
- if ((sender as FrameworkElement)?.DataContext is EditorToolViewModel tool)
- {
- m_SelectedTool = tool.Tool;
- foreach (var editorTool in m_EditorTools)
- editorTool.IsSelected = editorTool == tool;
- }
- }
-
- private void New_Click(object sender, RoutedEventArgs e)
- {
- m_Level = BuildStarterLevel();
- m_CurrentFile = null;
- m_SelectedCell = null;
- RefreshInspector();
- LevelCanvas.Invalidate();
- }
-
- private async void Open_Click(object sender, RoutedEventArgs args)
- {
- try
- {
- var picker = new FileOpenPicker();
- InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
- picker.FileTypeFilter.Add(".json");
-
- var file = await picker.PickSingleFileAsync();
- if (file is null)
- return;
-
- var json = await FileIO.ReadTextAsync(file);
- m_Level = LevelSerializer.Deserialize(json);
- m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
- m_CurrentFile = file;
- m_SelectedCell = null;
- RefreshInspector();
- LevelCanvas.Invalidate();
- }
- catch (Exception e)
- {
- var messageDialog = new MessageDialog(e.Message);
- _ = await messageDialog.ShowAsync();
- }
- }
-
- private async void Save_Click(object sender, RoutedEventArgs args)
- {
- try
- {
- var file = m_CurrentFile;
- if (file is null)
- {
- var picker = new FileSavePicker();
- InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
- picker.SuggestedFileName = m_Level.Name.Replace(' ', '-').ToLowerInvariant();
- picker.FileTypeChoices.Add("Reactor level", [".json"]);
- file = await picker.PickSaveFileAsync();
- }
-
- if (file is null)
- return;
-
- await FileIO.WriteTextAsync(file, LevelSerializer.Serialize(m_Level));
- m_CurrentFile = file;
- }
- catch (Exception e)
- {
- var messageDialog = new MessageDialog(e.Message);
- _ = await messageDialog.ShowAsync();
- }
- }
-
- private void Simulate_Click(object sender, RoutedEventArgs e)
- {
- m_Level = m_Simulation.AdvanceTurn(m_Level);
- RefreshInspector();
- LevelCanvas.Invalidate();
- }
-
- private void Activate_Click(object sender, RoutedEventArgs e)
- {
- m_Level = m_Simulation.ActivateReactor(m_Level);
- RefreshInspector();
- LevelCanvas.Invalidate();
- }
-
- private void LevelCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
- {
- var point = e.GetCurrentPoint(LevelCanvas);
- if (point.Properties.IsRightButtonPressed)
- {
- RemovePropAt(point.Position);
- e.Handled = true;
- return;
- }
-
- if (point.Properties.IsLeftButtonPressed)
- {
- _ = LevelCanvas.CapturePointer(e.Pointer);
- m_LeftPointerDown = true;
- m_LeftPointerDownPoint = point.Position;
- m_LastPanPoint = point.Position;
- m_DragExceededClickThreshold = false;
- e.Handled = true;
- }
- }
-
- private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
- {
- var point = e.GetCurrentPoint(LevelCanvas);
- if (m_LeftPointerDown)
- {
- var deltaX = point.Position.X - m_LastPanPoint.X;
- var deltaY = point.Position.Y - m_LastPanPoint.Y;
- m_LastPanPoint = point.Position;
-
- var totalDeltaX = point.Position.X - m_LeftPointerDownPoint.X;
- var totalDeltaY = point.Position.Y - m_LeftPointerDownPoint.Y;
- if (Math.Sqrt((totalDeltaX * totalDeltaX) + (totalDeltaY * totalDeltaY)) > c_ClickPixelThreshold)
- m_DragExceededClickThreshold = true;
-
- m_PanX += deltaX;
- m_PanY += deltaY;
- ClampPan();
- LevelCanvas.Invalidate();
- e.Handled = true;
- return;
- }
-
- SetHoveredCell(point.Position);
- }
-
- private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
- {
- var point = e.GetCurrentPoint(LevelCanvas);
- if (m_LeftPointerDown && !m_DragExceededClickThreshold)
- SelectOrPaintAt(point.Position);
-
- m_LeftPointerDown = false;
- m_DragExceededClickThreshold = false;
- LevelCanvas.ReleasePointerCapture(e.Pointer);
- e.Handled = true;
- }
-
- private void LevelCanvas_PointerExited(object sender, PointerRoutedEventArgs e)
- {
- ClearHoveredCell();
- }
-
- private void LevelCanvas_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
- {
- var point = e.GetCurrentPoint(LevelCanvas);
- var wheelDelta = point.Properties.MouseWheelDelta;
- if (wheelDelta == 0)
- return;
-
- ZoomAt(point.Position, wheelDelta > 0 ? c_ZoomStep : 1 / c_ZoomStep);
- e.Handled = true;
- }
-
- private void ZoomAt(Point point, double zoomFactor)
- {
- var oldLayout = GetLayout();
- var cellX = (point.X - oldLayout.OriginX) / oldLayout.CellSize;
- var cellY = (point.Y - oldLayout.OriginY) / oldLayout.CellSize;
-
- m_Zoom = Math.Clamp(m_Zoom * zoomFactor, c_MinZoom, c_MaxZoom);
- var newCellSize = GetBaseCellSize() * m_Zoom;
- var originWithoutPan = GetCenteredOrigin(newCellSize);
- m_PanX = point.X - originWithoutPan.X - (cellX * newCellSize);
- m_PanY = point.Y - originWithoutPan.Y - (cellY * newCellSize);
- ClampPan();
- LevelCanvas.Invalidate();
- }
-
- private void SelectOrPaintAt(Point point)
- {
- if (m_SelectedTool == EEditorTool.Cursor)
- SelectAt(point);
- else
- PaintAt(point);
- }
-
- private void SelectAt(Point point)
- {
- if (!TryGetGridPosition(point, out var position))
- return;
-
- m_SelectedCell = position;
- RefreshInspector();
- LevelCanvas.Invalidate();
- }
-
- private void RemovePropAt(Point point)
- {
- if (!TryGetGridPosition(point, out var position))
- return;
-
- var cell = m_Level.GetCell(position);
- m_SelectedCell = position;
- m_Level = m_Level.SetCell(position, cell with { Prop = ECellProp.None });
- m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
- RefreshInspector();
- LevelCanvas.Invalidate();
- }
-
- private void PaintAt(Point point)
- {
- if (!TryGetGridPosition(point, out var position))
- return;
-
- m_SelectedCell = position;
- m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
- m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
- RefreshInspector();
- LevelCanvas.Invalidate();
- }
-
- private void LevelCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
- {
- var drawing = args.DrawingSession;
- var layout = GetLayout();
-
- drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21));
- DrawTerrain(drawing, layout);
- DrawCellOverlays(drawing, layout);
- //DrawGrid(drawing, layout);
- DrawRobot(drawing, layout);
- }
-
- private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout)
- {
- for (var y = 0; y <= m_Level.Height; y++)
- {
- for (var x = 0; x <= m_Level.Width; x++)
- {
- DrawDualTerrainTile(drawing, layout.DualTileRect(x, y), GetDualTileMask(x, y));
- }
- }
- }
-
- private void DrawCellOverlays(CanvasDrawingSession drawing, CanvasLayout layout)
- {
- for (var y = 0; y < m_Level.Height; y++)
- {
- for (var x = 0; x < m_Level.Width; x++)
- {
- var position = new GridPosition(x, y);
- var cell = m_Level.GetCell(position);
- var rect = layout.CellRect(x, y);
-
- DrawPipe(drawing, position, cell, rect);
-
- if (cell.LeakRate > 0)
- DrawImage(drawing, m_LeakSprite, Inset(rect, 0.12));
-
- if (cell.Hazards.Heat > 0)
- DrawImage(drawing, m_HeatSprite, Inset(rect, 0.08), Math.Clamp(cell.Hazards.Heat / 10.0f, 0.35f, 0.9f));
-
- if (cell.Hazards.Fire)
- DrawImage(drawing, m_FireSprite, Inset(rect, 0.08));
-
- DrawCellProp(drawing, cell, rect);
-
- if (m_HoveredCell == position)
- drawing.FillRectangle(rect, ColorHelper.FromArgb(72, 255, 255, 255));
-
- if (m_SelectedCell == position)
- drawing.DrawRectangle(rect, Colors.White, 3);
- }
- }
- }
-
- private void DrawPipe(CanvasDrawingSession drawing, GridPosition position, CellState cell, Rect rect)
- {
- if (!cell.HasPipe || !m_PipeTilemaps.TryGetValue(cell.Pipe, out var tilemap))
- return;
-
- var sourceRect = PipeTileSourceRect(GetPipeConnectionMask(position, cell.Pipe));
- drawing.DrawImage(tilemap, rect, sourceRect, 1.0f, CanvasImageInterpolation.HighQualityCubic);
- }
-
- private int GetPipeConnectionMask(GridPosition position, EPipeMedium medium)
- {
- var mask = 0;
- if (HasMatchingPipe(position with { Y = position.Y - 1 }, medium))
- mask |= c_NorthConnection;
-
- if (HasMatchingPipe(position with { X = position.X + 1 }, medium))
- mask |= c_EastConnection;
-
- if (HasMatchingPipe(position with { Y = position.Y + 1 }, medium))
- mask |= c_SouthConnection;
-
- if (HasMatchingPipe(position with { X = position.X - 1 }, medium))
- mask |= c_WestConnection;
-
- return mask;
- }
-
- private bool HasMatchingPipe(GridPosition position, EPipeMedium medium)
- {
- return m_Level.InBounds(position) && m_Level.GetCell(position).Pipe == medium;
- }
-
- private static Rect PipeTileSourceRect(int connectionMask)
- {
- var tileIndex = connectionMask switch {
- 0 => 0,
- c_NorthConnection => 1,
- c_EastConnection => 2,
- c_SouthConnection => 3,
- c_WestConnection => 4,
- c_NorthConnection | c_EastConnection => 5,
- c_EastConnection | c_SouthConnection => 6,
- c_SouthConnection | c_WestConnection => 7,
- c_WestConnection | c_NorthConnection => 8,
- c_NorthConnection | c_SouthConnection => 9,
- c_EastConnection | c_WestConnection => 10,
- c_NorthConnection | c_EastConnection | c_SouthConnection => 11,
- c_EastConnection | c_SouthConnection | c_WestConnection => 12,
- c_SouthConnection | c_WestConnection | c_NorthConnection => 13,
- c_WestConnection | c_NorthConnection | c_EastConnection => 14,
- c_NorthConnection | c_EastConnection | c_SouthConnection | c_WestConnection => 15,
- _ => throw new ArgumentOutOfRangeException(nameof(connectionMask), connectionMask, "Unsupported pipe connection mask.")
- };
-
- return new(
- tileIndex % c_PipeTilemapColumns * c_PipeTilemapTileSize,
- tileIndex / c_PipeTilemapColumns * c_PipeTilemapTileSize,
- c_PipeTilemapTileSize,
- c_PipeTilemapTileSize);
- }
-
- private static void DrawImage(CanvasDrawingSession drawing, CanvasBitmap? image, Rect rect, float opacity = 1)
- {
- if (image is not null)
- drawing.DrawImage(image, rect, image.Bounds, opacity, CanvasImageInterpolation.HighQualityCubic);
- }
-
- private static Rect Inset(Rect rect, double fraction)
- {
- var inset = rect.Width * fraction;
- return new(rect.X + inset, rect.Y + inset, rect.Width - inset * 2, rect.Height - inset * 2);
- }
-
- private void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask)
- {
- if (m_TerrainTilemap is null)
- return;
-
- var wallMask = c_AllCorners ^ floorMask;
- var sourceRect = TilemapSourceRect(wallMask);
- drawing.DrawImage(m_TerrainTilemap, rect, sourceRect, 1.0f, CanvasImageInterpolation.HighQualityCubic);
- }
-
- private static Rect TilemapSourceRect(int wallMask)
- {
- var tilePosition = wallMask switch {
- c_BottomLeftCorner => new(0, 0),
- c_TopRightCorner | c_BottomRightCorner => new(1, 0),
- c_TopLeftCorner | c_BottomLeftCorner | c_BottomRightCorner => new(2, 0),
- c_BottomLeftCorner | c_BottomRightCorner => new(3, 0),
- c_TopLeftCorner | c_BottomRightCorner => new(0, 1),
- c_BottomLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(1, 1),
- c_AllCorners => new(2, 1),
- c_TopLeftCorner | c_BottomLeftCorner | c_TopRightCorner => new(3, 1),
- c_TopRightCorner => new(0, 2),
- c_TopLeftCorner | c_TopRightCorner => new(1, 2),
- c_TopLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(2, 2),
- c_BottomLeftCorner | c_TopLeftCorner => new(3, 2),
- 0 => new(0, 3),
- c_BottomRightCorner => new(1, 3),
- c_BottomLeftCorner | c_TopRightCorner => new(2, 3),
- c_TopLeftCorner => new GridPosition(3, 3),
- _ => throw new ArgumentOutOfRangeException(nameof(wallMask), wallMask, "Unsupported tile mask.")
- };
-
- return new(
- tilePosition.X * c_TilemapTileSize,
- tilePosition.Y * c_TilemapTileSize,
- c_TilemapTileSize,
- c_TilemapTileSize);
- }
-
- private int GetDualTileMask(int x, int y)
- {
- var mask = 0;
- if (GetTerrainOrWall(x - 1, y - 1) == ECellTerrain.Floor)
- mask |= c_TopLeftCorner;
-
- if (GetTerrainOrWall(x, y - 1) == ECellTerrain.Floor)
- mask |= c_TopRightCorner;
-
- if (GetTerrainOrWall(x - 1, y) == ECellTerrain.Floor)
- mask |= c_BottomLeftCorner;
-
- if (GetTerrainOrWall(x, y) == ECellTerrain.Floor)
- mask |= c_BottomRightCorner;
-
- return mask;
- }
-
- private ECellTerrain GetTerrainOrWall(int x, int y)
- {
- var position = new GridPosition(x, y);
- return m_Level.InBounds(position) ? m_Level.GetCell(position).Terrain : ECellTerrain.Wall;
- }
-
- private void DrawCellProp(CanvasDrawingSession drawing, CellState cell, Rect rect)
- {
- if (m_PropSprites.TryGetValue(cell.Prop, out var sprite))
- drawing.DrawImage(sprite, rect, sprite.Bounds, 1.0f, CanvasImageInterpolation.HighQualityCubic);
- }
-
- private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
- {
- for (var x = 0; x <= m_Level.Width; x++)
- {
- var xPos = (float)(layout.OriginX + (x * layout.CellSize));
- drawing.DrawLine(xPos, (float)layout.OriginY, xPos, (float)(layout.OriginY + (m_Level.Height * layout.CellSize)), ColorHelper.FromArgb(120, 91, 104, 115), 1);
- }
-
- for (var y = 0; y <= m_Level.Height; y++)
- {
- var yPos = (float)(layout.OriginY + (y * layout.CellSize));
- drawing.DrawLine((float)layout.OriginX, yPos, (float)(layout.OriginX + (m_Level.Width * layout.CellSize)), yPos, ColorHelper.FromArgb(120, 91, 104, 115), 1);
- }
- }
-
- private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout)
- {
- var rect = layout.CellRect(m_Level.Robot.X, m_Level.Robot.Y);
- DrawImage(drawing, m_RobotSprite, rect);
- }
-
- private bool TryGetGridPosition(Point point, out GridPosition position)
- {
- var layout = GetLayout();
- var x = (int)((point.X - layout.OriginX) / layout.CellSize);
- var y = (int)((point.Y - layout.OriginY) / layout.CellSize);
- position = new(x, y);
- return m_Level.InBounds(position);
- }
-
- private void SetHoveredCell(Point point)
- {
- var hoveredCell = TryGetGridPosition(point, out var position) ? position : (GridPosition?)null;
- if (m_HoveredCell == hoveredCell)
- return;
-
- m_HoveredCell = hoveredCell;
- LevelCanvas.Invalidate();
- }
-
- private void ClearHoveredCell()
- {
- if (m_HoveredCell is null)
- return;
-
- m_HoveredCell = null;
- LevelCanvas.Invalidate();
- }
-
- private CanvasLayout GetLayout()
- {
- ClampPan();
- var cellSize = GetBaseCellSize() * m_Zoom;
- var centeredOrigin = GetCenteredOrigin(cellSize);
- return new(cellSize, centeredOrigin.X + m_PanX, centeredOrigin.Y + m_PanY);
- }
-
- private double GetBaseCellSize()
- {
- var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
- var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
- var size = Math.Floor(Math.Min(availableWidth / m_Level.Width, availableHeight / m_Level.Height));
- return Math.Max(20, size);
- }
-
- private Point GetCenteredOrigin(double cellSize)
- {
- var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
- var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
- return new((availableWidth - (cellSize * m_Level.Width)) / 2, (availableHeight - (cellSize * m_Level.Height)) / 2);
- }
-
- private void ClampPan()
- {
- var cellSize = GetBaseCellSize() * m_Zoom;
- var contentWidth = cellSize * m_Level.Width;
- var contentHeight = cellSize * m_Level.Height;
- var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
- var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
-
- m_PanX = ClampAxisPan(m_PanX, contentWidth, availableWidth);
- m_PanY = ClampAxisPan(m_PanY, contentHeight, availableHeight);
- }
-
- private static double ClampAxisPan(double pan, double contentSize, double availableSize)
- {
- if (contentSize <= availableSize)
- return 0;
-
- var maxPan = (contentSize - availableSize) / 2;
- return Math.Clamp(pan, -maxPan, maxPan);
- }
-
- private void RefreshInspector()
- {
- LevelNameText.Text = m_Level.Name;
- TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
- StatusText.Text = m_Level.Global.Status;
- GlobalText.Text = $"Power: {m_Level.Global.Power}/10\n" + $"Cooling: {m_Level.Global.Cooling}/10\n" + $"Core Heat: {m_Level.Global.CoreHeat}/10\n" + $"Facility Stability: {m_Level.Global.FacilityStability}/10";
-
- if (m_SelectedCell is { } position && m_Level.InBounds(position))
- {
- var cell = m_Level.GetCell(position);
- CellText.Text = $"Position: {position.X},{position.Y}\n" + $"Terrain: {cell.Terrain}\n" + $"Prop: {cell.Prop}\n" + $"Pipe: {cell.Pipe}\n" + $"Flow: {cell.Flow}, Pressure: {cell.Pressure}\n" + $"Integrity: {cell.Integrity}, Leak: {cell.LeakRate}\n" + $"Heat: {cell.Hazards.Heat}, Smoke: {cell.Hazards.Smoke}\n" + $"Fuel Vapor: {cell.Hazards.FuelVapor}, Fuel: {cell.Hazards.LiquidFuel}\n" + $"Coolant: {cell.Hazards.CoolantPooling}, Charge: {cell.Hazards.ElectricalCharge}";
- }
- else
- CellText.Text = "No cell selected.";
-
- ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel(FailureIcon(forecast.Kind), forecast.Message)).ToArray();
- }
-
- private static BitmapImage FailureIcon(EFailureKind kind)
- {
- return ImageFromOutputPath("Images", "Failures", FailureIconFileName(kind));
- }
-
- private static string FailureIconFileName(EFailureKind kind)
- {
- return kind switch {
- EFailureKind.PipeBurst => "failure-pipe-burst.png",
- EFailureKind.Ignition => "failure-ignition.png",
- EFailureKind.Meltdown => "failure-meltdown.png",
- EFailureKind.StabilityCollapse => "failure-stability-collapse.png",
- EFailureKind.ReactorReady => "failure-reactor-ready.png",
- _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unsupported failure kind.")
- };
- }
-
- private static BitmapImage? EditorToolIcon(EEditorTool tool)
- {
- return tool switch {
- EEditorTool.Cursor => PropImage("cursor.png"),
- EEditorTool.Floor => PropImage("floor.png"),
- EEditorTool.Wall => PropImage("wall.png"),
- EEditorTool.Reactor => PropImage("reactor.png"),
- EEditorTool.CoolingPump => PropImage("cooling-pump.png"),
- EEditorTool.Generator => PropImage("generator.png"),
- EEditorTool.PressureRegulator => PropImage("pressure-regulator.png"),
- EEditorTool.DiagnosticTerminal => PropImage("diagnostic-terminal.png"),
- EEditorTool.ControlTerminal => PropImage("control-terminal.png"),
- EEditorTool.CoolantPipe => PipeImage("pipe-coolant-tilemap.png"),
- EEditorTool.FuelPipe => PipeImage("pipe-fuel-tilemap.png"),
- EEditorTool.PressurePipe => PipeImage("pipe-pressure-tilemap.png"),
- EEditorTool.Leak => PropImage("leak.png"),
- EEditorTool.Repair => PropImage("repair.png"),
- EEditorTool.Heat => PropImage("heat.png"),
- EEditorTool.Fire => PropImage("fire.png"),
- EEditorTool.Robot => PropImage("robot.png"),
- _ => throw new ArgumentOutOfRangeException(nameof(tool), tool, "Unsupported editor tool.")
- };
- }
-
- private static BitmapImage PropImage(string fileName)
- {
- return ImageFromOutputPath("Images", "Props", fileName);
- }
-
- private static BitmapImage PipeImage(string fileName)
- {
- return ImageFromOutputPath("Images", "Pipes", fileName);
- }
-
- private static BitmapImage ImageFromOutputPath(params string[] pathParts)
- {
- return new(new(Path.Combine([AppContext.BaseDirectory, .. pathParts])));
- }
-
- private static LevelState BuildStarterLevel()
- {
- var level = LevelState.Create("Cooling Sector B", 16, 12);
- level = level.SetCell(new(3, 5), new() {
- Prop = ECellProp.CoolingPump,
- Pipe = EPipeMedium.Coolant,
- Flow = 5,
- Pressure = 5,
- Powered = true
- });
- level = level.SetCell(new(4, 5), new() {
- Pipe = EPipeMedium.Coolant,
- Flow = 5,
- Pressure = 7
- });
- level = level.SetCell(new(5, 5), new() {
- Pipe = EPipeMedium.Coolant,
- Flow = 3,
- Pressure = 8,
- LeakRate = 2,
- Integrity = 4
- });
- level = level.SetCell(new(6, 5), new() {
- Pipe = EPipeMedium.Coolant,
- Flow = 3,
- Pressure = 7
- });
- level = level.SetCell(new(8, 5), new() {
- Prop = ECellProp.Reactor,
- Hazards = new() {
- Heat = 6,
- Stability = 8
- }
- });
- level = level.SetCell(new(2, 8), new() {
- Prop = ECellProp.Generator,
- Pipe = EPipeMedium.Fuel,
- Flow = 4,
- Pressure = 6,
- Powered = true
- });
- level = level.SetCell(new(11, 4), new() {
- Prop = ECellProp.DiagnosticTerminal,
- Powered = true
- });
- level = level.SetCell(new(12, 8), new() {
- Prop = ECellProp.ControlTerminal,
- Powered = true
- });
- return level with { Forecasts = new SimulationEngine().Forecast(level) };
- }
-
- private const int c_TilemapTileSize = 512;
- private const int c_PipeTilemapTileSize = 256;
- private const int c_PipeTilemapColumns = 4;
- private const int c_TopLeftCorner = 1;
- private const int c_TopRightCorner = 2;
- private const int c_BottomLeftCorner = 4;
- private const int c_BottomRightCorner = 8;
- private const int c_AllCorners = c_TopLeftCorner | c_TopRightCorner | c_BottomLeftCorner | c_BottomRightCorner;
- private const int c_NorthConnection = 1;
- private const int c_EastConnection = 2;
- private const int c_SouthConnection = 4;
- private const int c_WestConnection = 8;
- private const double c_MinZoom = 0.5;
- private const double c_MaxZoom = 4;
- private const double c_ZoomStep = 1.15;
- private const double c_ClickPixelThreshold = 5;
-
- private readonly SimulationEngine m_Simulation = new();
- private readonly Dictionary m_PropSprites = [];
- private readonly Dictionary m_PipeTilemaps = [];
- private StorageFile? m_CurrentFile;
- private LevelState m_Level;
- private IReadOnlyList m_EditorTools = [];
- private bool m_LeftPointerDown;
- private bool m_DragExceededClickThreshold;
- private Point m_LeftPointerDownPoint;
- private Point m_LastPanPoint;
- private GridPosition? m_HoveredCell;
- private GridPosition? m_SelectedCell;
- private EEditorTool m_SelectedTool = EEditorTool.Cursor;
- private double m_Zoom = 1;
- private double m_PanX;
- private double m_PanY;
- private CanvasBitmap? m_TerrainTilemap;
- private CanvasBitmap? m_RobotSprite;
- private CanvasBitmap? m_LeakSprite;
- private CanvasBitmap? m_HeatSprite;
- private CanvasBitmap? m_FireSprite;
-}
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.UI;
+using Microsoft.Graphics.Canvas.UI.Xaml;
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media.Imaging;
+using ReactorMaintenance.Simulation;
+using System.Globalization;
+using Windows.Foundation;
+using Windows.Storage;
+using Windows.Storage.Pickers;
+using Windows.UI;
+using Windows.UI.Popups;
+using WinRT.Interop;
+
+namespace ReactorMaintenance.Win2D;
+
+public sealed partial class MainWindow
+{
+ private sealed record CanvasLayout(double CellSize, double OriginX, double OriginY)
+ {
+ public Rect CellRect(int x, int y)
+ {
+ return new(OriginX + (x * CellSize), OriginY + (y * CellSize), CellSize, CellSize);
+ }
+
+ public Rect DualTileRect(int x, int y)
+ {
+ return new(OriginX + ((x - 0.5) * CellSize), OriginY + ((y - 0.5) * CellSize), CellSize, CellSize);
+ }
+ }
+
+ private sealed record ForecastViewModel(BitmapImage Icon, string Message);
+
+ private sealed class EditorToolViewModel(EEditorTool tool, BitmapImage? icon, string label)
+ {
+ public EEditorTool Tool { get; } = tool;
+ public BitmapImage? Icon { get; } = icon;
+ public string Label { get; } = label;
+ public bool IsSelected { get; set; }
+ }
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ m_Level = BuildStarterLevel();
+ m_EditorTools = Enum.GetValues().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString()) { IsSelected = tool == m_SelectedTool }).ToArray();
+ ToolPicker.ItemsSource = m_EditorTools;
+ RefreshInspector();
+ }
+
+ private void LevelCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
+ {
+ args.TrackAsyncAction(LoadImagesAsync(sender).AsAsyncAction());
+ }
+
+ private async Task LoadImagesAsync(CanvasControl sender)
+ {
+ m_TerrainTilemap = await LoadCanvasBitmapAsync(sender, "Images", "tilemap.png");
+ m_RobotSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "robot.png");
+ m_LeakSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "leak.png");
+ m_HeatSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "heat.png");
+ m_FireSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "fire.png");
+
+ m_PropSprites[ECellProp.Reactor] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "reactor.png");
+ m_PropSprites[ECellProp.CoolingPump] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "cooling-pump.png");
+ m_PropSprites[ECellProp.Generator] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "generator.png");
+ m_PropSprites[ECellProp.PressureRegulator] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "pressure-regulator.png");
+ m_PropSprites[ECellProp.DiagnosticTerminal] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "diagnostic-terminal.png");
+ m_PropSprites[ECellProp.ControlTerminal] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "control-terminal.png");
+
+ m_PipeTilemaps[EPipeMedium.Pressure] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-pressure-tilemap.png");
+ m_PipeTilemaps[EPipeMedium.Coolant] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-coolant-tilemap.png");
+ m_PipeTilemaps[EPipeMedium.Fuel] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-fuel-tilemap.png");
+ }
+
+ private static async Task LoadCanvasBitmapAsync(CanvasControl sender, params string[] pathParts)
+ {
+ var path = Path.Combine([AppContext.BaseDirectory, .. pathParts]);
+ return await CanvasBitmap.LoadAsync(sender, path);
+ }
+
+ private void ToolToggle_Checked(object sender, RoutedEventArgs e)
+ {
+ if ((sender as FrameworkElement)?.DataContext is EditorToolViewModel tool)
+ {
+ m_SelectedTool = tool.Tool;
+ foreach (var editorTool in m_EditorTools)
+ editorTool.IsSelected = editorTool == tool;
+ }
+ }
+
+ private void New_Click(object sender, RoutedEventArgs e)
+ {
+ m_Level = BuildStarterLevel();
+ m_CurrentFile = null;
+ m_SelectedCell = null;
+ RefreshInspector();
+ LevelCanvas.Invalidate();
+ }
+
+ private async void Open_Click(object sender, RoutedEventArgs args)
+ {
+ try
+ {
+ var picker = new FileOpenPicker();
+ InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
+ picker.FileTypeFilter.Add(".json");
+
+ var file = await picker.PickSingleFileAsync();
+ if (file is null)
+ return;
+
+ var json = await FileIO.ReadTextAsync(file);
+ m_Level = LevelSerializer.Deserialize(json);
+ m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
+ m_CurrentFile = file;
+ m_SelectedCell = null;
+ RefreshInspector();
+ LevelCanvas.Invalidate();
+ }
+ catch (Exception e)
+ {
+ var messageDialog = new MessageDialog(e.Message);
+ _ = await messageDialog.ShowAsync();
+ }
+ }
+
+ private async void Save_Click(object sender, RoutedEventArgs args)
+ {
+ try
+ {
+ var file = m_CurrentFile;
+ if (file is null)
+ {
+ var picker = new FileSavePicker();
+ InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
+ picker.SuggestedFileName = m_Level.Name.Replace(' ', '-').ToLowerInvariant();
+ picker.FileTypeChoices.Add("Reactor level", [".json"]);
+ file = await picker.PickSaveFileAsync();
+ }
+
+ if (file is null)
+ return;
+
+ await FileIO.WriteTextAsync(file, LevelSerializer.Serialize(m_Level));
+ m_CurrentFile = file;
+ }
+ catch (Exception e)
+ {
+ var messageDialog = new MessageDialog(e.Message);
+ _ = await messageDialog.ShowAsync();
+ }
+ }
+
+ private void Simulate_Click(object sender, RoutedEventArgs e)
+ {
+ m_Level = m_Simulation.AdvanceTurn(m_Level);
+ RefreshInspector();
+ LevelCanvas.Invalidate();
+ }
+
+ private void Activate_Click(object sender, RoutedEventArgs e)
+ {
+ m_Level = m_Simulation.ActivateReactor(m_Level);
+ RefreshInspector();
+ LevelCanvas.Invalidate();
+ }
+
+ private void LevelCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
+ {
+ var point = e.GetCurrentPoint(LevelCanvas);
+ if (point.Properties.IsRightButtonPressed)
+ {
+ RemovePropAt(point.Position);
+ e.Handled = true;
+ return;
+ }
+
+ if (point.Properties.IsLeftButtonPressed)
+ {
+ _ = LevelCanvas.CapturePointer(e.Pointer);
+ m_LeftPointerDown = true;
+ m_LeftPointerDownPoint = point.Position;
+ m_LastPanPoint = point.Position;
+ m_DragExceededClickThreshold = false;
+ e.Handled = true;
+ }
+ }
+
+ private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
+ {
+ var point = e.GetCurrentPoint(LevelCanvas);
+ if (m_LeftPointerDown)
+ {
+ var deltaX = point.Position.X - m_LastPanPoint.X;
+ var deltaY = point.Position.Y - m_LastPanPoint.Y;
+ m_LastPanPoint = point.Position;
+
+ var totalDeltaX = point.Position.X - m_LeftPointerDownPoint.X;
+ var totalDeltaY = point.Position.Y - m_LeftPointerDownPoint.Y;
+ if (Math.Sqrt((totalDeltaX * totalDeltaX) + (totalDeltaY * totalDeltaY)) > c_ClickPixelThreshold)
+ m_DragExceededClickThreshold = true;
+
+ m_PanX += deltaX;
+ m_PanY += deltaY;
+ ClampPan();
+ LevelCanvas.Invalidate();
+ e.Handled = true;
+ return;
+ }
+ }
+
+ private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
+ {
+ var point = e.GetCurrentPoint(LevelCanvas);
+ if (m_LeftPointerDown && !m_DragExceededClickThreshold)
+ SelectOrPaintAt(point.Position);
+
+ m_LeftPointerDown = false;
+ m_DragExceededClickThreshold = false;
+ LevelCanvas.ReleasePointerCapture(e.Pointer);
+ e.Handled = true;
+ }
+
+ private void LevelCanvas_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
+ {
+ var point = e.GetCurrentPoint(LevelCanvas);
+ var wheelDelta = point.Properties.MouseWheelDelta;
+ if (wheelDelta == 0)
+ return;
+
+ ZoomAt(point.Position, wheelDelta > 0 ? c_ZoomStep : 1 / c_ZoomStep);
+ e.Handled = true;
+ }
+
+ private void ZoomAt(Point point, double zoomFactor)
+ {
+ var oldLayout = GetLayout();
+ var cellX = (point.X - oldLayout.OriginX) / oldLayout.CellSize;
+ var cellY = (point.Y - oldLayout.OriginY) / oldLayout.CellSize;
+
+ m_Zoom = Math.Clamp(m_Zoom * zoomFactor, c_MinZoom, c_MaxZoom);
+ var newCellSize = GetBaseCellSize() * m_Zoom;
+ var originWithoutPan = GetCenteredOrigin(newCellSize);
+ m_PanX = point.X - originWithoutPan.X - (cellX * newCellSize);
+ m_PanY = point.Y - originWithoutPan.Y - (cellY * newCellSize);
+ ClampPan();
+ LevelCanvas.Invalidate();
+ }
+
+ private void SelectOrPaintAt(Point point)
+ {
+ if (m_SelectedTool == EEditorTool.Cursor)
+ SelectAt(point);
+ else
+ PaintAt(point);
+ }
+
+ private void SelectAt(Point point)
+ {
+ if (!TryGetGridPosition(point, out var position))
+ return;
+
+ m_SelectedCell = position;
+ RefreshInspector();
+ LevelCanvas.Invalidate();
+ }
+
+ private void RemovePropAt(Point point)
+ {
+ if (!TryGetGridPosition(point, out var position))
+ return;
+
+ var cell = m_Level.GetCell(position);
+ m_SelectedCell = position;
+ m_Level = m_Level.SetCell(position, cell with { Prop = ECellProp.None });
+ m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
+ RefreshInspector();
+ LevelCanvas.Invalidate();
+ }
+
+ private void PaintAt(Point point)
+ {
+ if (!TryGetGridPosition(point, out var position))
+ return;
+
+ m_SelectedCell = position;
+ m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
+ m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
+ RefreshInspector();
+ LevelCanvas.Invalidate();
+ }
+
+ private void LevelCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
+ {
+ var drawing = args.DrawingSession;
+ var layout = GetLayout();
+
+ drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21));
+ DrawTerrain(drawing, layout);
+ DrawCellOverlays(drawing, layout);
+ //DrawGrid(drawing, layout);
+ DrawRobot(drawing, layout);
+ }
+
+ private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout)
+ {
+ for (var y = 0; y <= m_Level.Height; y++)
+ {
+ for (var x = 0; x <= m_Level.Width; x++)
+ {
+ DrawDualTerrainTile(drawing, layout.DualTileRect(x, y), GetDualTileMask(x, y));
+ }
+ }
+ }
+
+ private void DrawCellOverlays(CanvasDrawingSession drawing, CanvasLayout layout)
+ {
+ for (var y = 0; y < m_Level.Height; y++)
+ {
+ for (var x = 0; x < m_Level.Width; x++)
+ {
+ var position = new GridPosition(x, y);
+ var cell = m_Level.GetCell(position);
+ var rect = layout.CellRect(x, y);
+
+ DrawPipe(drawing, position, cell, rect);
+
+ if (cell.LeakRate > 0)
+ DrawImage(drawing, m_LeakSprite, Inset(rect, 0.12));
+
+ if (cell.Hazards.Heat > 0)
+ DrawImage(drawing, m_HeatSprite, Inset(rect, 0.08), Math.Clamp(cell.Hazards.Heat / 10.0f, 0.35f, 0.9f));
+
+ if (cell.Hazards.Fire)
+ DrawImage(drawing, m_FireSprite, Inset(rect, 0.08));
+
+ if (m_SelectedCell == position)
+ drawing.DrawRectangle(rect, Colors.White, 3);
+
+ DrawCellProp(drawing, cell, rect);
+ }
+ }
+ }
+
+ private void DrawPipe(CanvasDrawingSession drawing, GridPosition position, CellState cell, Rect rect)
+ {
+ if (!cell.HasPipe || !m_PipeTilemaps.TryGetValue(cell.Pipe, out var tilemap))
+ return;
+
+ var sourceRect = PipeTileSourceRect(GetPipeConnectionMask(position, cell.Pipe));
+ drawing.DrawImage(tilemap, rect, sourceRect, 1.0f, CanvasImageInterpolation.HighQualityCubic);
+ }
+
+ private int GetPipeConnectionMask(GridPosition position, EPipeMedium medium)
+ {
+ var mask = 0;
+ if (HasMatchingPipe(position with { Y = position.Y - 1 }, medium))
+ mask |= c_NorthConnection;
+
+ if (HasMatchingPipe(position with { X = position.X + 1 }, medium))
+ mask |= c_EastConnection;
+
+ if (HasMatchingPipe(position with { Y = position.Y + 1 }, medium))
+ mask |= c_SouthConnection;
+
+ if (HasMatchingPipe(position with { X = position.X - 1 }, medium))
+ mask |= c_WestConnection;
+
+ return mask;
+ }
+
+ private bool HasMatchingPipe(GridPosition position, EPipeMedium medium)
+ {
+ return m_Level.InBounds(position) && m_Level.GetCell(position).Pipe == medium;
+ }
+
+ private static Rect PipeTileSourceRect(int connectionMask)
+ {
+ var tileIndex = connectionMask switch {
+ 0 => 0,
+ c_NorthConnection => 1,
+ c_EastConnection => 2,
+ c_SouthConnection => 3,
+ c_WestConnection => 4,
+ c_NorthConnection | c_EastConnection => 5,
+ c_EastConnection | c_SouthConnection => 6,
+ c_SouthConnection | c_WestConnection => 7,
+ c_WestConnection | c_NorthConnection => 8,
+ c_NorthConnection | c_SouthConnection => 9,
+ c_EastConnection | c_WestConnection => 10,
+ c_NorthConnection | c_EastConnection | c_SouthConnection => 11,
+ c_EastConnection | c_SouthConnection | c_WestConnection => 12,
+ c_SouthConnection | c_WestConnection | c_NorthConnection => 13,
+ c_WestConnection | c_NorthConnection | c_EastConnection => 14,
+ c_NorthConnection | c_EastConnection | c_SouthConnection | c_WestConnection => 15,
+ _ => throw new ArgumentOutOfRangeException(nameof(connectionMask), connectionMask, "Unsupported pipe connection mask.")
+ };
+
+ return new(
+ tileIndex % c_PipeTilemapColumns * c_PipeTilemapTileSize,
+ tileIndex / c_PipeTilemapColumns * c_PipeTilemapTileSize,
+ c_PipeTilemapTileSize,
+ c_PipeTilemapTileSize);
+ }
+
+ private static void DrawImage(CanvasDrawingSession drawing, CanvasBitmap? image, Rect rect, float opacity = 1)
+ {
+ if (image is not null)
+ drawing.DrawImage(image, rect, image.Bounds, opacity, CanvasImageInterpolation.HighQualityCubic);
+ }
+
+ private static Rect Inset(Rect rect, double fraction)
+ {
+ var inset = rect.Width * fraction;
+ return new(rect.X + inset, rect.Y + inset, rect.Width - inset * 2, rect.Height - inset * 2);
+ }
+
+ private void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask)
+ {
+ if (m_TerrainTilemap is null)
+ return;
+
+ var wallMask = c_AllCorners ^ floorMask;
+ var sourceRect = TilemapSourceRect(wallMask);
+ drawing.DrawImage(m_TerrainTilemap, rect, sourceRect, 1.0f, CanvasImageInterpolation.HighQualityCubic);
+ }
+
+ private static Rect TilemapSourceRect(int wallMask)
+ {
+ var tilePosition = wallMask switch {
+ c_BottomLeftCorner => new(0, 0),
+ c_TopRightCorner | c_BottomRightCorner => new(1, 0),
+ c_TopLeftCorner | c_BottomLeftCorner | c_BottomRightCorner => new(2, 0),
+ c_BottomLeftCorner | c_BottomRightCorner => new(3, 0),
+ c_TopLeftCorner | c_BottomRightCorner => new(0, 1),
+ c_BottomLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(1, 1),
+ c_AllCorners => new(2, 1),
+ c_TopLeftCorner | c_BottomLeftCorner | c_TopRightCorner => new(3, 1),
+ c_TopRightCorner => new(0, 2),
+ c_TopLeftCorner | c_TopRightCorner => new(1, 2),
+ c_TopLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(2, 2),
+ c_BottomLeftCorner | c_TopLeftCorner => new(3, 2),
+ 0 => new(0, 3),
+ c_BottomRightCorner => new(1, 3),
+ c_BottomLeftCorner | c_TopRightCorner => new(2, 3),
+ c_TopLeftCorner => new GridPosition(3, 3),
+ _ => throw new ArgumentOutOfRangeException(nameof(wallMask), wallMask, "Unsupported tile mask.")
+ };
+
+ return new(
+ tilePosition.X * c_TilemapTileSize,
+ tilePosition.Y * c_TilemapTileSize,
+ c_TilemapTileSize,
+ c_TilemapTileSize);
+ }
+
+ private int GetDualTileMask(int x, int y)
+ {
+ var mask = 0;
+ if (GetTerrainOrWall(x - 1, y - 1) == ECellTerrain.Floor)
+ mask |= c_TopLeftCorner;
+
+ if (GetTerrainOrWall(x, y - 1) == ECellTerrain.Floor)
+ mask |= c_TopRightCorner;
+
+ if (GetTerrainOrWall(x - 1, y) == ECellTerrain.Floor)
+ mask |= c_BottomLeftCorner;
+
+ if (GetTerrainOrWall(x, y) == ECellTerrain.Floor)
+ mask |= c_BottomRightCorner;
+
+ return mask;
+ }
+
+ private ECellTerrain GetTerrainOrWall(int x, int y)
+ {
+ var position = new GridPosition(x, y);
+ return m_Level.InBounds(position) ? m_Level.GetCell(position).Terrain : ECellTerrain.Wall;
+ }
+
+ private void DrawCellProp(CanvasDrawingSession drawing, CellState cell, Rect rect)
+ {
+ if (m_PropSprites.TryGetValue(cell.Prop, out var sprite))
+ drawing.DrawImage(sprite, rect, sprite.Bounds, 1.0f, CanvasImageInterpolation.HighQualityCubic);
+ }
+
+ private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
+ {
+ for (var x = 0; x <= m_Level.Width; x++)
+ {
+ var xPos = (float)(layout.OriginX + (x * layout.CellSize));
+ drawing.DrawLine(xPos, (float)layout.OriginY, xPos, (float)(layout.OriginY + (m_Level.Height * layout.CellSize)), ColorHelper.FromArgb(120, 91, 104, 115), 1);
+ }
+
+ for (var y = 0; y <= m_Level.Height; y++)
+ {
+ var yPos = (float)(layout.OriginY + (y * layout.CellSize));
+ drawing.DrawLine((float)layout.OriginX, yPos, (float)(layout.OriginX + (m_Level.Width * layout.CellSize)), yPos, ColorHelper.FromArgb(120, 91, 104, 115), 1);
+ }
+ }
+
+ private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout)
+ {
+ var rect = layout.CellRect(m_Level.Robot.X, m_Level.Robot.Y);
+ DrawImage(drawing, m_RobotSprite, rect);
+ }
+
+ private bool TryGetGridPosition(Point point, out GridPosition position)
+ {
+ var layout = GetLayout();
+ var x = (int)((point.X - layout.OriginX) / layout.CellSize);
+ var y = (int)((point.Y - layout.OriginY) / layout.CellSize);
+ position = new(x, y);
+ return m_Level.InBounds(position);
+ }
+
+ private CanvasLayout GetLayout()
+ {
+ ClampPan();
+ var cellSize = GetBaseCellSize() * m_Zoom;
+ var centeredOrigin = GetCenteredOrigin(cellSize);
+ return new(cellSize, centeredOrigin.X + m_PanX, centeredOrigin.Y + m_PanY);
+ }
+
+ private double GetBaseCellSize()
+ {
+ var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
+ var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
+ var size = Math.Floor(Math.Min(availableWidth / m_Level.Width, availableHeight / m_Level.Height));
+ return Math.Max(20, size);
+ }
+
+ private Point GetCenteredOrigin(double cellSize)
+ {
+ var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
+ var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
+ return new((availableWidth - (cellSize * m_Level.Width)) / 2, (availableHeight - (cellSize * m_Level.Height)) / 2);
+ }
+
+ private void ClampPan()
+ {
+ var cellSize = GetBaseCellSize() * m_Zoom;
+ var contentWidth = cellSize * m_Level.Width;
+ var contentHeight = cellSize * m_Level.Height;
+ var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
+ var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
+
+ m_PanX = ClampAxisPan(m_PanX, contentWidth, availableWidth);
+ m_PanY = ClampAxisPan(m_PanY, contentHeight, availableHeight);
+ }
+
+ private static double ClampAxisPan(double pan, double contentSize, double availableSize)
+ {
+ if (contentSize <= availableSize)
+ return 0;
+
+ var maxPan = (contentSize - availableSize) / 2;
+ return Math.Clamp(pan, -maxPan, maxPan);
+ }
+
+ private void RefreshInspector()
+ {
+ LevelNameText.Text = m_Level.Name;
+ TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
+ StatusText.Text = m_Level.Global.Status;
+ GlobalText.Text = $"Power: {m_Level.Global.Power}/10\n" + $"Cooling: {m_Level.Global.Cooling}/10\n" + $"Core Heat: {m_Level.Global.CoreHeat}/10\n" + $"Facility Stability: {m_Level.Global.FacilityStability}/10";
+
+ if (m_SelectedCell is { } position && m_Level.InBounds(position))
+ {
+ var cell = m_Level.GetCell(position);
+ CellText.Text = $"Position: {position.X},{position.Y}\n" + $"Terrain: {cell.Terrain}\n" + $"Prop: {cell.Prop}\n" + $"Pipe: {cell.Pipe}\n" + $"Flow: {cell.Flow}, Pressure: {cell.Pressure}\n" + $"Integrity: {cell.Integrity}, Leak: {cell.LeakRate}\n" + $"Heat: {cell.Hazards.Heat}, Smoke: {cell.Hazards.Smoke}\n" + $"Fuel Vapor: {cell.Hazards.FuelVapor}, Fuel: {cell.Hazards.LiquidFuel}\n" + $"Coolant: {cell.Hazards.CoolantPooling}, Charge: {cell.Hazards.ElectricalCharge}";
+ }
+ else
+ CellText.Text = "No cell selected.";
+
+ ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel(FailureIcon(forecast.Kind), forecast.Message)).ToArray();
+ }
+
+ private static BitmapImage FailureIcon(EFailureKind kind)
+ {
+ return ImageFromOutputPath("Images", "Failures", FailureIconFileName(kind));
+ }
+
+ private static string FailureIconFileName(EFailureKind kind)
+ {
+ return kind switch {
+ EFailureKind.PipeBurst => "failure-pipe-burst.png",
+ EFailureKind.Ignition => "failure-ignition.png",
+ EFailureKind.Meltdown => "failure-meltdown.png",
+ EFailureKind.StabilityCollapse => "failure-stability-collapse.png",
+ EFailureKind.ReactorReady => "failure-reactor-ready.png",
+ _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unsupported failure kind.")
+ };
+ }
+
+ private static BitmapImage? EditorToolIcon(EEditorTool tool)
+ {
+ return tool switch {
+ EEditorTool.Cursor => PropImage("cursor.png"),
+ EEditorTool.Floor => PropImage("floor.png"),
+ EEditorTool.Wall => PropImage("wall.png"),
+ EEditorTool.Reactor => PropImage("reactor.png"),
+ EEditorTool.CoolingPump => PropImage("cooling-pump.png"),
+ EEditorTool.Generator => PropImage("generator.png"),
+ EEditorTool.PressureRegulator => PropImage("pressure-regulator.png"),
+ EEditorTool.DiagnosticTerminal => PropImage("diagnostic-terminal.png"),
+ EEditorTool.ControlTerminal => PropImage("control-terminal.png"),
+ EEditorTool.CoolantPipe => PipeImage("pipe-coolant-tilemap.png"),
+ EEditorTool.FuelPipe => PipeImage("pipe-fuel-tilemap.png"),
+ EEditorTool.PressurePipe => PipeImage("pipe-pressure-tilemap.png"),
+ EEditorTool.Leak => PropImage("leak.png"),
+ EEditorTool.Repair => PropImage("repair.png"),
+ EEditorTool.Heat => PropImage("heat.png"),
+ EEditorTool.Fire => PropImage("fire.png"),
+ EEditorTool.Robot => PropImage("robot.png"),
+ _ => throw new ArgumentOutOfRangeException(nameof(tool), tool, "Unsupported editor tool.")
+ };
+ }
+
+ private static BitmapImage PropImage(string fileName)
+ {
+ return ImageFromOutputPath("Images", "Props", fileName);
+ }
+
+ private static BitmapImage PipeImage(string fileName)
+ {
+ return ImageFromOutputPath("Images", "Pipes", fileName);
+ }
+
+ private static BitmapImage ImageFromOutputPath(params string[] pathParts)
+ {
+ return new(new(Path.Combine([AppContext.BaseDirectory, .. pathParts])));
+ }
+
+ private static LevelState BuildStarterLevel()
+ {
+ var level = LevelState.Create("Cooling Sector B", 16, 12);
+ level = level.SetCell(new(3, 5), new() {
+ Prop = ECellProp.CoolingPump,
+ Pipe = EPipeMedium.Coolant,
+ Flow = 5,
+ Pressure = 5,
+ Powered = true
+ });
+ level = level.SetCell(new(4, 5), new() {
+ Pipe = EPipeMedium.Coolant,
+ Flow = 5,
+ Pressure = 7
+ });
+ level = level.SetCell(new(5, 5), new() {
+ Pipe = EPipeMedium.Coolant,
+ Flow = 3,
+ Pressure = 8,
+ LeakRate = 2,
+ Integrity = 4
+ });
+ level = level.SetCell(new(6, 5), new() {
+ Pipe = EPipeMedium.Coolant,
+ Flow = 3,
+ Pressure = 7
+ });
+ level = level.SetCell(new(8, 5), new() {
+ Prop = ECellProp.Reactor,
+ Hazards = new() {
+ Heat = 6,
+ Stability = 8
+ }
+ });
+ level = level.SetCell(new(2, 8), new() {
+ Prop = ECellProp.Generator,
+ Pipe = EPipeMedium.Fuel,
+ Flow = 4,
+ Pressure = 6,
+ Powered = true
+ });
+ level = level.SetCell(new(11, 4), new() {
+ Prop = ECellProp.DiagnosticTerminal,
+ Powered = true
+ });
+ level = level.SetCell(new(12, 8), new() {
+ Prop = ECellProp.ControlTerminal,
+ Powered = true
+ });
+ return level with { Forecasts = new SimulationEngine().Forecast(level) };
+ }
+
+ private const int c_TilemapTileSize = 512;
+ private const int c_PipeTilemapTileSize = 256;
+ private const int c_PipeTilemapColumns = 4;
+ private const int c_TopLeftCorner = 1;
+ private const int c_TopRightCorner = 2;
+ private const int c_BottomLeftCorner = 4;
+ private const int c_BottomRightCorner = 8;
+ private const int c_AllCorners = c_TopLeftCorner | c_TopRightCorner | c_BottomLeftCorner | c_BottomRightCorner;
+ private const int c_NorthConnection = 1;
+ private const int c_EastConnection = 2;
+ private const int c_SouthConnection = 4;
+ private const int c_WestConnection = 8;
+ private const double c_MinZoom = 0.5;
+ private const double c_MaxZoom = 4;
+ private const double c_ZoomStep = 1.15;
+ private const double c_ClickPixelThreshold = 10;
+
+ private readonly SimulationEngine m_Simulation = new();
+ private readonly Dictionary m_PropSprites = [];
+ private readonly Dictionary m_PipeTilemaps = [];
+ private StorageFile? m_CurrentFile;
+ private LevelState m_Level;
+ private IReadOnlyList m_EditorTools = [];
+ private bool m_LeftPointerDown;
+ private bool m_DragExceededClickThreshold;
+ private Point m_LeftPointerDownPoint;
+ private Point m_LastPanPoint;
+ private GridPosition? m_SelectedCell;
+ private EEditorTool m_SelectedTool = EEditorTool.Cursor;
+ private double m_Zoom = 1;
+ private double m_PanX;
+ private double m_PanY;
+ private CanvasBitmap? m_TerrainTilemap;
+ private CanvasBitmap? m_RobotSprite;
+ private CanvasBitmap? m_LeakSprite;
+ private CanvasBitmap? m_HeatSprite;
+ private CanvasBitmap? m_FireSprite;
+}
diff --git a/src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj b/src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj
index 727591e..3d8ca5a 100644
--- a/src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj
+++ b/src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj
@@ -1,33 +1,33 @@
-
-
- WinExe
- net8.0-windows10.0.19041.0
- 10.0.17763.0
- ReactorMaintenance.Win2D
- app.manifest
- x86;x64;arm64
- win-x86;win-x64;win-arm64
- true
- enable
- enable
- None
- true
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- PreserveNewest
-
-
-
+
+
+ WinExe
+ net8.0-windows10.0.19041.0
+ 10.0.17763.0
+ ReactorMaintenance.Win2D
+ app.manifest
+ x86;x64;arm64
+ win-x86;win-x64;win-arm64
+ true
+ enable
+ enable
+ None
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/src/ReactorMaintenance.Win2D/app.manifest b/src/ReactorMaintenance.Win2D/app.manifest
index 6ad7df3..dd5896e 100644
--- a/src/ReactorMaintenance.Win2D/app.manifest
+++ b/src/ReactorMaintenance.Win2D/app.manifest
@@ -1,10 +1,10 @@
-
-
-
-
-
- true/pm
- PerMonitorV2
-
-
-
+
+
+
+
+
+ true/pm
+ PerMonitorV2
+
+
+
diff --git a/tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj b/tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj
index 5edf316..e6d83c1 100644
--- a/tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj
+++ b/tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj
@@ -1,27 +1,27 @@
-
-
-
- net8.0
- enable
- enable
-
- false
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs
index 6a2cacb..6bae661 100644
--- a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs
+++ b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs
@@ -1,298 +1,298 @@
-using ReactorMaintenance.Simulation.Difficulties;
-using ReactorMaintenance.Simulation.Effects;
-using ReactorMaintenance.Simulation.Hazards;
-
-namespace ReactorMaintenance.Simulation.Tests;
-
-public sealed class SimulationEngineTests
-{
- [Fact]
- public void FuelLeakNearPoweredGeneratorCreatesIgnitionForecast()
- {
- var level = LevelState.Create("Fuel leak", 6, 6)
- .SetCell(new(2, 2), new() {
- Prop = ECellProp.Generator,
- Pipe = EPipeMedium.Fuel,
- LeakRate = Balancing.Current.FuelVaporFireThreshold,
- Pressure = Balancing.Current.PressurizedFuelLeakPressureThreshold + Balancing.Current.NeighborDistance,
- Integrity = Balancing.Current.DefaultEditedPipeIntegrity,
- Powered = true
- });
-
- var forecasts = m_Engine.Forecast(level);
-
- Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Ignition && forecast.Position == new GridPosition(2, 2) && forecast.Turns == 1);
- }
-
- [Fact]
- public void CoolantLeakOnPoweredCellRaisesElectricalCharge()
- {
- var level = LevelState.Create("Wet cable", 6, 6)
- .SetCell(new(3, 3), new() {
- Pipe = EPipeMedium.Coolant,
- LeakRate = Balancing.Current.ElectrifiedCoolantPoolingThreshold,
- Powered = true
- });
-
- var next = m_Engine.AdvanceTurn(level);
-
- Assert.True(next.GetCell(new(3, 3)).Hazards.ElectricalCharge >= Balancing.Current.ElectricalChargeIncrease);
- }
-
- [Fact]
- public void ActiveFireSpreadsSmokeToOpenNeighbors()
- {
- var level = LevelState.Create("Smoke", 6, 6)
- .SetCell(new(2, 2), new() {
- Hazards = new() {
- Fire = true,
- Smoke = Balancing.Current.SmokeSpreadThreshold
- }
- });
-
- var next = m_Engine.AdvanceTurn(level);
-
- Assert.True(next.GetCell(new(2, 3)).Hazards.Smoke > 0);
- }
-
- [Fact]
- public void AdvanceTurnRunsConfiguredCellEffects()
- {
- var engine = new SimulationEngine([new TestCellEffect()], [], []);
- var level = LevelState.Create("Custom effect", 6, 6)
- .SetCell(new(2, 2), new() {
- Hazards = new() { Heat = 1 }
- });
-
- var next = engine.AdvanceTurn(level);
-
- Assert.Equal(5, next.GetCell(new(2, 2)).Hazards.Heat);
- }
-
- [Fact]
- public void AdvanceTurnRunsConfiguredAreaEffects()
- {
- var engine = new SimulationEngine([], [new TestAreaEffect()], []);
- var level = LevelState.Create("Custom area effect", 6, 6);
-
- var next = engine.AdvanceTurn(level);
-
- Assert.Equal(7, next.GetCell(new(2, 2)).Hazards.Smoke);
- }
-
- [Fact]
- public void OverpressurePredictsPipeBurst()
- {
- var level = LevelState.Create("Pressure", 6, 6)
- .SetCell(new(1, 2), new() {
- Pipe = EPipeMedium.Pressure,
- Pressure = 10,
- Integrity = 6
- });
-
- var forecasts = m_Engine.Forecast(level);
-
- Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.PipeBurst && forecast.Turns == 2);
- }
-
- [Fact]
- public void ForecastCapsFutureSimulationWhenNoTerminalConditionOccurs()
- {
- var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
- var level = LevelState.Create("Stable", 6, 6);
-
- var forecasts = engine.Forecast(level);
-
- Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
- Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Max(forecast => forecast.Turns));
- }
-
- [Fact]
- public void ForecastUsesCurrentBalancingProfile()
- {
- var previous = Balancing.Current;
- try
- {
- Balancing.Current = new TestBalancing();
- var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
- var level = LevelState.Create("Stable", 6, 6);
-
- var forecasts = engine.Forecast(level);
-
- Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
- }
- finally
- {
- Balancing.Current = previous;
- }
- }
-
- [Fact]
- public void ForecastPredictsMeltdownFromFutureSimulation()
- {
- var level = LevelState.Create("Meltdown", 6, 6)
- .SetCell(new(2, 2), new() {
- Prop = ECellProp.Reactor,
- Hazards = new() { Heat = Balancing.Current.MeltdownCoreHeatThreshold - Balancing.Current.ReactorHeatIncrease }
- });
-
- var forecasts = m_Engine.Forecast(level);
-
- Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 1);
- }
-
- [Fact]
- public void ForecastReportsAlreadyLostLevelAtCurrentTurn()
- {
- var level = LevelState.Create("Lost", 6, 6) with {
- Global = new() {
- CoreHeat = Balancing.Current.MeltdownCoreHeatThreshold,
- Lost = true,
- Status = "CORE MELTDOWN"
- }
- };
-
- var forecasts = m_Engine.Forecast(level);
-
- Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 0);
- }
-
- [Fact]
- public void ForecastPredictsStabilityCollapseFromFutureSimulation()
- {
- var level = LevelState.Create("Collapse", 6, 6)
- .SetCell(new(2, 2), new() {
- Prop = ECellProp.Generator,
- Hazards = new() { Stability = Balancing.Current.CriticalCellStabilityThreshold }
- }) with {
- Global = new() { FacilityStability = Balancing.Current.FireStabilityDamage }
- };
-
- var forecasts = m_Engine.Forecast(level);
-
- Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.StabilityCollapse && forecast.Turns == 1);
- }
-
- [Fact]
- public void StableReactorWithPowerAndCoolingCanActivate()
- {
- var level = LevelState.Create("Ready", 8, 6)
- .SetCell(new(2, 2), new() {
- Prop = ECellProp.Reactor,
- Hazards = new() { Heat = 3 }
- })
- .SetCell(new(3, 2), new() {
- Prop = ECellProp.Generator,
- Powered = true
- })
- .SetCell(new(4, 2), new() {
- Prop = ECellProp.CoolingPump,
- Powered = true
- });
-
- var next = m_Engine.AdvanceTurn(level);
- var activated = m_Engine.ActivateReactor(next);
-
- Assert.Equal("REACTOR ONLINE", activated.Global.Status);
- Assert.True(activated.Global.ReactorActivated);
- }
-
- [Fact]
- public void LevelSerializationRoundTripsEditableState()
- {
- var level = LevelState.Create("Round trip", 5, 5);
- level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Reactor);
- level = LevelEditor.Apply(level, new(1, 2), EEditorTool.CoolantPipe);
- level = LevelEditor.Apply(level, new(1, 2), EEditorTool.Leak);
-
- var json = LevelSerializer.Serialize(level);
- var loaded = LevelSerializer.Deserialize(json);
-
- Assert.Contains("\"Version\": 1", json);
- Assert.Equal(level.Name, loaded.Name);
- Assert.Equal(ECellProp.Reactor, loaded.GetCell(new(2, 2)).Prop);
- Assert.Equal(EPipeMedium.Coolant, loaded.GetCell(new(1, 2)).Pipe);
- Assert.Equal(1, loaded.GetCell(new(1, 2)).LeakRate);
- }
-
- [Fact]
- public void LevelSerializationRejectsUnsupportedVersion()
- {
- var json = """
- {
- "Version": 999,
- "Level": {}
- }
- """;
-
- var exception = Assert.Throws(() => LevelSerializer.Deserialize(json));
-
- Assert.Contains("Unsupported level file version 999", exception.Message);
- }
-
- [Fact]
- public void WallToolClearsCellPropsPipesAndHazards()
- {
- var level = LevelState.Create("Wall", 5, 5);
- level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Generator);
- level = LevelEditor.Apply(level, new(2, 2), EEditorTool.CoolantPipe);
- level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Fire);
-
- var edited = LevelEditor.Apply(level, new(2, 2), EEditorTool.Wall);
- var cell = edited.GetCell(new(2, 2));
-
- Assert.Equal(ECellTerrain.Wall, cell.Terrain);
- Assert.Equal(ECellProp.None, cell.Prop);
- Assert.Equal(EPipeMedium.None, cell.Pipe);
- Assert.False(cell.Powered);
- Assert.False(cell.Hazards.Fire);
- }
-
- [Fact]
- public void PropToolsKeepFloorTerrain()
- {
- var level = LevelState.Create("Prop", 5, 5);
- level = LevelEditor.Apply(level, new(1, 1), EEditorTool.Wall);
-
- var edited = LevelEditor.Apply(level, new(1, 1), EEditorTool.Reactor);
- var cell = edited.GetCell(new(1, 1));
-
- Assert.Equal(ECellTerrain.Floor, cell.Terrain);
- Assert.Equal(ECellProp.Reactor, cell.Prop);
- }
-
- private readonly SimulationEngine m_Engine = new();
-
- private sealed class StepCountingHazard : Hazard
- {
- public override IEnumerable Predict(LevelState level, int turns)
- {
- yield return new(EFailureKind.PipeBurst, new(turns, 0), turns, $"STEP {turns}");
- }
- }
-
- private sealed class TestBalancing : NormalBalancing
- {
- public override int MaxForecastStepCount => 2;
- }
-
- private sealed class TestCellEffect : ISimulationEffect
- {
- public CellState Apply(CellState cell)
- {
- return cell with { Hazards = cell.Hazards with { Heat = 5 } };
- }
- }
-
- private sealed class TestAreaEffect : IAreaSimulationEffect
- {
- public CellState[] Apply(LevelState level, CellState[] cells)
- {
- var next = cells.ToArray();
- var position = new GridPosition(2, 2);
- var cell = next[level.Index(position)];
- next[level.Index(position)] = cell with { Hazards = cell.Hazards with { Smoke = 7 } };
- return next;
- }
- }
+using ReactorMaintenance.Simulation.Difficulties;
+using ReactorMaintenance.Simulation.Effects;
+using ReactorMaintenance.Simulation.Hazards;
+
+namespace ReactorMaintenance.Simulation.Tests;
+
+public sealed class SimulationEngineTests
+{
+ [Fact]
+ public void FuelLeakNearPoweredGeneratorCreatesIgnitionForecast()
+ {
+ var level = LevelState.Create("Fuel leak", 6, 6)
+ .SetCell(new(2, 2), new() {
+ Prop = ECellProp.Generator,
+ Pipe = EPipeMedium.Fuel,
+ LeakRate = Balancing.Current.FuelVaporFireThreshold,
+ Pressure = Balancing.Current.PressurizedFuelLeakPressureThreshold + Balancing.Current.NeighborDistance,
+ Integrity = Balancing.Current.DefaultEditedPipeIntegrity,
+ Powered = true
+ });
+
+ var forecasts = m_Engine.Forecast(level);
+
+ Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Ignition && forecast.Position == new GridPosition(2, 2) && forecast.Turns == 1);
+ }
+
+ [Fact]
+ public void CoolantLeakOnPoweredCellRaisesElectricalCharge()
+ {
+ var level = LevelState.Create("Wet cable", 6, 6)
+ .SetCell(new(3, 3), new() {
+ Pipe = EPipeMedium.Coolant,
+ LeakRate = Balancing.Current.ElectrifiedCoolantPoolingThreshold,
+ Powered = true
+ });
+
+ var next = m_Engine.AdvanceTurn(level);
+
+ Assert.True(next.GetCell(new(3, 3)).Hazards.ElectricalCharge >= Balancing.Current.ElectricalChargeIncrease);
+ }
+
+ [Fact]
+ public void ActiveFireSpreadsSmokeToOpenNeighbors()
+ {
+ var level = LevelState.Create("Smoke", 6, 6)
+ .SetCell(new(2, 2), new() {
+ Hazards = new() {
+ Fire = true,
+ Smoke = Balancing.Current.SmokeSpreadThreshold
+ }
+ });
+
+ var next = m_Engine.AdvanceTurn(level);
+
+ Assert.True(next.GetCell(new(2, 3)).Hazards.Smoke > 0);
+ }
+
+ [Fact]
+ public void AdvanceTurnRunsConfiguredCellEffects()
+ {
+ var engine = new SimulationEngine([new TestCellEffect()], [], []);
+ var level = LevelState.Create("Custom effect", 6, 6)
+ .SetCell(new(2, 2), new() {
+ Hazards = new() { Heat = 1 }
+ });
+
+ var next = engine.AdvanceTurn(level);
+
+ Assert.Equal(5, next.GetCell(new(2, 2)).Hazards.Heat);
+ }
+
+ [Fact]
+ public void AdvanceTurnRunsConfiguredAreaEffects()
+ {
+ var engine = new SimulationEngine([], [new TestAreaEffect()], []);
+ var level = LevelState.Create("Custom area effect", 6, 6);
+
+ var next = engine.AdvanceTurn(level);
+
+ Assert.Equal(7, next.GetCell(new(2, 2)).Hazards.Smoke);
+ }
+
+ [Fact]
+ public void OverpressurePredictsPipeBurst()
+ {
+ var level = LevelState.Create("Pressure", 6, 6)
+ .SetCell(new(1, 2), new() {
+ Pipe = EPipeMedium.Pressure,
+ Pressure = 10,
+ Integrity = 6
+ });
+
+ var forecasts = m_Engine.Forecast(level);
+
+ Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.PipeBurst && forecast.Turns == 2);
+ }
+
+ [Fact]
+ public void ForecastCapsFutureSimulationWhenNoTerminalConditionOccurs()
+ {
+ var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
+ var level = LevelState.Create("Stable", 6, 6);
+
+ var forecasts = engine.Forecast(level);
+
+ Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
+ Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Max(forecast => forecast.Turns));
+ }
+
+ [Fact]
+ public void ForecastUsesCurrentBalancingProfile()
+ {
+ var previous = Balancing.Current;
+ try
+ {
+ Balancing.Current = new TestBalancing();
+ var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
+ var level = LevelState.Create("Stable", 6, 6);
+
+ var forecasts = engine.Forecast(level);
+
+ Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
+ }
+ finally
+ {
+ Balancing.Current = previous;
+ }
+ }
+
+ [Fact]
+ public void ForecastPredictsMeltdownFromFutureSimulation()
+ {
+ var level = LevelState.Create("Meltdown", 6, 6)
+ .SetCell(new(2, 2), new() {
+ Prop = ECellProp.Reactor,
+ Hazards = new() { Heat = Balancing.Current.MeltdownCoreHeatThreshold - Balancing.Current.ReactorHeatIncrease }
+ });
+
+ var forecasts = m_Engine.Forecast(level);
+
+ Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 1);
+ }
+
+ [Fact]
+ public void ForecastReportsAlreadyLostLevelAtCurrentTurn()
+ {
+ var level = LevelState.Create("Lost", 6, 6) with {
+ Global = new() {
+ CoreHeat = Balancing.Current.MeltdownCoreHeatThreshold,
+ Lost = true,
+ Status = "CORE MELTDOWN"
+ }
+ };
+
+ var forecasts = m_Engine.Forecast(level);
+
+ Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 0);
+ }
+
+ [Fact]
+ public void ForecastPredictsStabilityCollapseFromFutureSimulation()
+ {
+ var level = LevelState.Create("Collapse", 6, 6)
+ .SetCell(new(2, 2), new() {
+ Prop = ECellProp.Generator,
+ Hazards = new() { Stability = Balancing.Current.CriticalCellStabilityThreshold }
+ }) with {
+ Global = new() { FacilityStability = Balancing.Current.FireStabilityDamage }
+ };
+
+ var forecasts = m_Engine.Forecast(level);
+
+ Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.StabilityCollapse && forecast.Turns == 1);
+ }
+
+ [Fact]
+ public void StableReactorWithPowerAndCoolingCanActivate()
+ {
+ var level = LevelState.Create("Ready", 8, 6)
+ .SetCell(new(2, 2), new() {
+ Prop = ECellProp.Reactor,
+ Hazards = new() { Heat = 3 }
+ })
+ .SetCell(new(3, 2), new() {
+ Prop = ECellProp.Generator,
+ Powered = true
+ })
+ .SetCell(new(4, 2), new() {
+ Prop = ECellProp.CoolingPump,
+ Powered = true
+ });
+
+ var next = m_Engine.AdvanceTurn(level);
+ var activated = m_Engine.ActivateReactor(next);
+
+ Assert.Equal("REACTOR ONLINE", activated.Global.Status);
+ Assert.True(activated.Global.ReactorActivated);
+ }
+
+ [Fact]
+ public void LevelSerializationRoundTripsEditableState()
+ {
+ var level = LevelState.Create("Round trip", 5, 5);
+ level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Reactor);
+ level = LevelEditor.Apply(level, new(1, 2), EEditorTool.CoolantPipe);
+ level = LevelEditor.Apply(level, new(1, 2), EEditorTool.Leak);
+
+ var json = LevelSerializer.Serialize(level);
+ var loaded = LevelSerializer.Deserialize(json);
+
+ Assert.Contains("\"Version\": 1", json);
+ Assert.Equal(level.Name, loaded.Name);
+ Assert.Equal(ECellProp.Reactor, loaded.GetCell(new(2, 2)).Prop);
+ Assert.Equal(EPipeMedium.Coolant, loaded.GetCell(new(1, 2)).Pipe);
+ Assert.Equal(1, loaded.GetCell(new(1, 2)).LeakRate);
+ }
+
+ [Fact]
+ public void LevelSerializationRejectsUnsupportedVersion()
+ {
+ var json = """
+ {
+ "Version": 999,
+ "Level": {}
+ }
+ """;
+
+ var exception = Assert.Throws(() => LevelSerializer.Deserialize(json));
+
+ Assert.Contains("Unsupported level file version 999", exception.Message);
+ }
+
+ [Fact]
+ public void WallToolClearsCellPropsPipesAndHazards()
+ {
+ var level = LevelState.Create("Wall", 5, 5);
+ level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Generator);
+ level = LevelEditor.Apply(level, new(2, 2), EEditorTool.CoolantPipe);
+ level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Fire);
+
+ var edited = LevelEditor.Apply(level, new(2, 2), EEditorTool.Wall);
+ var cell = edited.GetCell(new(2, 2));
+
+ Assert.Equal(ECellTerrain.Wall, cell.Terrain);
+ Assert.Equal(ECellProp.None, cell.Prop);
+ Assert.Equal(EPipeMedium.None, cell.Pipe);
+ Assert.False(cell.Powered);
+ Assert.False(cell.Hazards.Fire);
+ }
+
+ [Fact]
+ public void PropToolsKeepFloorTerrain()
+ {
+ var level = LevelState.Create("Prop", 5, 5);
+ level = LevelEditor.Apply(level, new(1, 1), EEditorTool.Wall);
+
+ var edited = LevelEditor.Apply(level, new(1, 1), EEditorTool.Reactor);
+ var cell = edited.GetCell(new(1, 1));
+
+ Assert.Equal(ECellTerrain.Floor, cell.Terrain);
+ Assert.Equal(ECellProp.Reactor, cell.Prop);
+ }
+
+ private readonly SimulationEngine m_Engine = new();
+
+ private sealed class StepCountingHazard : Hazard
+ {
+ public override IEnumerable Predict(LevelState level, int turns)
+ {
+ yield return new(EFailureKind.PipeBurst, new(turns, 0), turns, $"STEP {turns}");
+ }
+ }
+
+ private sealed class TestBalancing : NormalBalancing
+ {
+ public override int MaxForecastStepCount => 2;
+ }
+
+ private sealed class TestCellEffect : ISimulationEffect
+ {
+ public CellState Apply(CellState cell)
+ {
+ return cell with { Hazards = cell.Hazards with { Heat = 5 } };
+ }
+ }
+
+ private sealed class TestAreaEffect : IAreaSimulationEffect
+ {
+ public CellState[] Apply(LevelState level, CellState[] cells)
+ {
+ var next = cells.ToArray();
+ var position = new GridPosition(2, 2);
+ var cell = next[level.Index(position)];
+ next[level.Index(position)] = cell with { Hazards = cell.Hazards with { Smoke = 7 } };
+ return next;
+ }
+ }
}
\ No newline at end of file