diff --git a/code/__DEFINES/atmospherics.dm b/code/__DEFINES/atmospherics.dm
index e481b30b8abb3..0b9ab4bee5213 100644
--- a/code/__DEFINES/atmospherics.dm
+++ b/code/__DEFINES/atmospherics.dm
@@ -326,6 +326,15 @@ GLOBAL_LIST_INIT(atmos_adjacent_savings, list(0,0))
#define GAS_ETHANOL "ethanol"
#define GAS_MOTOR_OIL "motor_oil" // BLUEMOON ADD - Напитки для синтетиков
#define GAS_QCD "qcd"
+// HFR / fusion gases (from WhiteMoon HFR port)
+#define GAS_HELIUM "helium"
+#define GAS_FREON "freon"
+#define GAS_HALON "halon"
+#define GAS_ANTINOBLIUM "antinoblium"
+#define GAS_PROTO_NITRATE "proto_nitrate"
+#define GAS_ZAUKER "zauker"
+#define GAS_HEALIUM "healium"
+#define GAS_NITRIUM "nitrium"
#define GAS_GROUP_CHEMICALS "Chemicals"
diff --git a/code/__DEFINES/construction.dm b/code/__DEFINES/construction.dm
index 45ab215e30058..f486ba9ac1853 100644
--- a/code/__DEFINES/construction.dm
+++ b/code/__DEFINES/construction.dm
@@ -105,6 +105,9 @@
#define CAT_SPAGHETTI "Spaghettis"
#define CAT_ICE "Frozen"
#define CAT_EAST "East foods" //BLUEMOON ADD
+// Category tab; single subcategory for crystallizer crystals and gas crafts (BLUEMOON ADD)
+#define CAT_ATMOSPHERIC "Atmospherics"
+#define CAT_ATMOSPHERICS "Gas Crystals"
#define RCD_FLOORWALL 1
#define RCD_AIRLOCK 2
diff --git a/code/__DEFINES/inventory.dm b/code/__DEFINES/inventory.dm
index d0532d658b5a3..fcc625627e521 100644
--- a/code/__DEFINES/inventory.dm
+++ b/code/__DEFINES/inventory.dm
@@ -88,6 +88,8 @@
//sandstorm edit
#define HIDEUNDERWEAR (1<<14) //hides underwear, socks and shirt
#define HIDEWRISTS (1<<15) //hides wrists
+#define HIDEBACK (1<<16) //suit obscures back slot (no backpack/tank on back)
+#define ALLOWS_BACK_TANK (1<<17) //suit explicitly allows back slot despite being full-body
//
//bitflags for clothing coverage - also used for limbs
diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm
index b047f189b0fbe..1128354c19733 100644
--- a/code/__DEFINES/logging.dm
+++ b/code/__DEFINES/logging.dm
@@ -19,6 +19,7 @@
#define INVESTIGATE_RESEARCH "research"
#define INVESTIGATE_SINGULO "singulo"
#define INVESTIGATE_SUPERMATTER "supermatter"
+#define INVESTIGATE_HYPERTORUS "hypertorus"
#define INVESTIGATE_TELESCI "telesci"
#define INVESTIGATE_WIRES "wires"
diff --git a/code/__DEFINES/reactions.dm b/code/__DEFINES/reactions.dm
index 061ac289d050e..5cdc9b9aae4e4 100644
--- a/code/__DEFINES/reactions.dm
+++ b/code/__DEFINES/reactions.dm
@@ -21,8 +21,9 @@
#define STIMULUM_FIRST_DROP 0.065
#define STIMULUM_SECOND_RISE 0.0009
#define STIMULUM_ABSOLUTE_DROP 0.00000335
-#define REACTION_OPPRESSION_THRESHOLD 10
-#define NOBLIUM_FORMATION_ENERGY 2e9 //1 Mole of Noblium takes the planck energy to condense.
+#define REACTION_OPPRESSION_THRESHOLD 5 // stops reactions when >5 mol and temp > 20 K
+#define NOBLIUM_FORMATION_ENERGY 2e9 // energy released per mole (exothermic); BZ reduces amount
+#define NOBLIUM_FORMATION_MAX_TEMP 15 // below 15 K only
//Research point amounts
#define NOBLIUM_RESEARCH_AMOUNT 25
#define BZ_RESEARCH_SCALE 4
@@ -43,5 +44,71 @@
#define FUSION_RAD_MAX 2000
#define FUSION_RAD_COEFFICIENT (-1000)
#define FUSION_INSTABILITY_ENDOTHERMALITY 2
+#define FUSION_MAXIMUM_TEMPERATURE 1e8
// Snowflake fire product types
#define FIRE_PRODUCT_PLASMA 0
+
+// Freon — below 0°C (273.15 K) endothermic with O2, down to ~50 K; Proto-Nitrate catalyst up to 310 K; hot ice 120–160 K
+#define FREON_MAXIMUM_BURN_TEMPERATURE T0C
+#define FREON_CATALYST_MAX_TEMPERATURE 310
+#define FREON_LOWER_TEMPERATURE 60
+#define FREON_TERMINAL_TEMPERATURE 50
+#define FREON_HOT_ICE_MIN_TEMP 120
+#define FREON_HOT_ICE_MAX_TEMP 160
+#define FREON_OXYGEN_FULLBURN 10
+#define FREON_BURN_RATE_DELTA 4
+#define FIRE_FREON_ENERGY_CONSUMED 3e5
+#define FREON_FORMATION_MIN_TEMPERATURE (FIRE_MINIMUM_TEMPERATURE_TO_EXIST + 100)
+#define FREON_FORMATION_ENERGY_CONSUMED 2e5
+#define OXYGEN_BURN_RATIO_BASE 2
+
+// Halon
+#define HALON_COMBUSTION_ENERGY 2500
+#define HALON_COMBUSTION_MIN_TEMPERATURE (T0C + 70)
+#define HALON_COMBUSTION_TEMPERATURE_SCALE (FIRE_MINIMUM_TEMPERATURE_TO_EXIST * 10)
+#define HALON_COMBUSTION_MINIMUM_RESIN_MOLES (0.99 * HALON_COMBUSTION_MIN_TEMPERATURE / HALON_COMBUSTION_TEMPERATURE_SCALE)
+
+// Healium
+#define HEALIUM_FORMATION_MIN_TEMP 25
+#define HEALIUM_FORMATION_MAX_TEMP 300
+#define HEALIUM_FORMATION_ENERGY 9000
+
+// Zauker
+#define ZAUKER_FORMATION_MIN_TEMPERATURE 50000
+#define ZAUKER_FORMATION_MAX_TEMPERATURE 75000
+#define ZAUKER_FORMATION_TEMPERATURE_SCALE 5e-6
+#define ZAUKER_FORMATION_ENERGY 5000
+#define ZAUKER_DECOMPOSITION_MAX_RATE 20
+#define ZAUKER_DECOMPOSITION_ENERGY 460
+
+// Nitrium
+#define NITRIUM_FORMATION_MIN_TEMP 1500
+#define NITRIUM_FORMATION_TEMP_DIVISOR (FIRE_MINIMUM_TEMPERATURE_TO_EXIST * 8)
+#define NITRIUM_FORMATION_ENERGY 100000
+#define NITRIUM_DECOMPOSITION_MAX_TEMP (T0C + 70)
+#define NITRIUM_DECOMPOSITION_TEMP_DIVISOR (FIRE_MINIMUM_TEMPERATURE_TO_EXIST * 8)
+#define NITRIUM_DECOMPOSITION_ENERGY 30000
+
+// Pluoxium formation (CO2 + O2 + Tritium)
+#define PLUOXIUM_FORMATION_MIN_TEMP 50
+#define PLUOXIUM_FORMATION_MAX_TEMP T0C
+#define PLUOXIUM_FORMATION_MAX_RATE 5
+#define PLUOXIUM_FORMATION_ENERGY 250
+
+// Proto-Nitrate
+#define PN_FORMATION_MIN_TEMPERATURE 5000
+#define PN_FORMATION_MAX_TEMPERATURE 10000
+#define PN_FORMATION_ENERGY 650
+#define PN_HYDROGEN_CONVERSION_THRESHOLD 150
+#define PN_HYDROGEN_CONVERSION_MAX_RATE 5
+#define PN_HYDROGEN_CONVERSION_ENERGY 2500
+#define PN_TRITIUM_CONVERSION_MIN_TEMP 150
+#define PN_TRITIUM_CONVERSION_MAX_TEMP 340
+#define PN_TRITIUM_CONVERSION_ENERGY 10000
+#define PN_BZASE_MIN_TEMP 260
+#define PN_BZASE_MAX_TEMP 280
+#define PN_BZASE_ENERGY 60000
+
+// Antinoblium
+#define ANTINOBLIUM_CONVERSION_DIVISOR 90
+#define REACTION_OPPRESSION_MIN_TEMP 20
diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm
index 8ddfd38cccc77..76512fde4fabb 100644
--- a/code/__DEFINES/sound.dm
+++ b/code/__DEFINES/sound.dm
@@ -507,3 +507,5 @@ GLOBAL_LIST_EMPTY(sfx_datum_by_key)
#define SFX_DRAWER_CLOSE "drawer_close"
#define SFX_ROLLING_PIN_ROLLING "rolling_pin_rolling"
#define SFX_KNIFE_SLICE "knife_slice"
+#define SFX_HYPERTORUS_CALM "hypertorus_calm"
+#define SFX_HYPERTORUS_MELTING "hypertorus_melting"
diff --git a/code/datums/components/crafting/crafting.dm b/code/datums/components/crafting/crafting.dm
index 28d9510990053..e2c31fc848a78 100644
--- a/code/datums/components/crafting/crafting.dm
+++ b/code/datums/components/crafting/crafting.dm
@@ -25,6 +25,9 @@
CAT_TOOL,
CAT_FURNITURE,
),
+ CAT_ATMOSPHERIC = list(
+ CAT_ATMOSPHERICS,
+ ),
CAT_PRIMAL = CAT_NONE,
CAT_FOOD = list(
CAT_BREAD,
diff --git a/code/datums/components/crafting/recipes/recipes_atmospheric_crystals.dm b/code/datums/components/crafting/recipes/recipes_atmospheric_crystals.dm
new file mode 100644
index 0000000000000..0b3eb0c706eb1
--- /dev/null
+++ b/code/datums/components/crafting/recipes/recipes_atmospheric_crystals.dm
@@ -0,0 +1,142 @@
+// Крафты из продуктов кристаллайзера и атмос-оборудования (вкладка Atmospherics)
+
+/datum/crafting_recipe/zaukerite_bolt
+ name = "Zaukerite bolt"
+ result = /obj/item/zaukerite_bolt
+ reqs = list(
+ /obj/item/stack/sheet/mineral/zaukerite = 1,
+ /obj/item/stack/rods = 1,
+ )
+ time = 25
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+/datum/crafting_recipe/hot_ice_pack
+ name = "Hot ice cooling pack"
+ result = /obj/item/hot_ice_pack
+ reqs = list(
+ /obj/item/stack/sheet/hot_ice = 3,
+ /obj/item/stack/sheet/cloth = 2,
+ )
+ time = 30
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+// --- Atmos equipment (из WhiteMoon tailoring.dm + atmospheric.dm) ---
+/datum/crafting_recipe/atmospherics_gas_mask
+ name = "Atmospherics gas mask"
+ result = /obj/item/clothing/mask/gas/atmos
+ tools = list(TOOL_WELDER)
+ time = 80
+ reqs = list(
+ /obj/item/stack/sheet/mineral/metal_hydrogen = 1,
+ /obj/item/stack/sheet/mineral/zaukerite = 1,
+ )
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+/datum/crafting_recipe/igniter
+ name = "Igniter"
+ result = /obj/machinery/igniter
+ reqs = list(
+ /obj/item/stack/sheet/metal = 5,
+ /obj/item/assembly/igniter = 1,
+ )
+ time = 20
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+/datum/crafting_recipe/ammonia_pack
+ name = "Ammonia pack"
+ result = /obj/item/ammonia_pack
+ reqs = list(
+ /obj/item/stack/ammonia_crystals = 3,
+ /obj/item/stack/sheet/cloth = 2,
+ )
+ time = 25
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+/datum/crafting_recipe/metallic_hydrogen_rod
+ name = "Metallic hydrogen rod"
+ result = /obj/item/metallic_hydrogen_rod
+ reqs = list(
+ /obj/item/stack/sheet/mineral/metal_hydrogen = 1,
+ /obj/item/stack/rods = 1,
+ )
+ time = 30
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+/datum/crafting_recipe/metallic_hydrogen_cooling_pack
+ name = "Metallic hydrogen cooling pack"
+ result = /obj/item/metallic_hydrogen_cooling_pack
+ reqs = list(
+ /obj/item/stack/sheet/mineral/metal_hydrogen = 2,
+ /obj/item/stack/sheet/cloth = 2,
+ )
+ time = 35
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+/datum/crafting_recipe/elder_atmosian_statue
+ name = "Elder Atmosian statue"
+ result = /obj/structure/statue/elder_atmosian
+ reqs = list(
+ /obj/item/stack/sheet/mineral/metal_hydrogen = 20,
+ /obj/item/stack/sheet/mineral/zaukerite = 15,
+ /obj/item/stack/sheet/metal = 30,
+ )
+ time = 60
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+/datum/crafting_recipe/elder_atmosian_armor
+ name = "Elder Atmosian armor"
+ result = /obj/item/clothing/suit/armor/elder_atmosian
+ reqs = list(
+ /obj/item/stack/sheet/mineral/metal_hydrogen = 5,
+ /obj/item/stack/sheet/mineral/zaukerite = 3,
+ /obj/item/stack/sheet/metal = 10,
+ /obj/item/clothing/suit/fire/atmos = 1,
+ )
+ time = 40
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+/datum/crafting_recipe/elder_atmosian_helmet
+ name = "Elder Atmosian helmet"
+ result = /obj/item/clothing/head/helmet/elder_atmosian
+ reqs = list(
+ /obj/item/stack/sheet/mineral/metal_hydrogen = 3,
+ /obj/item/stack/sheet/mineral/zaukerite = 2,
+ /obj/item/stack/sheet/metal = 5,
+ /obj/item/clothing/head/hardhat/atmos = 1,
+ )
+ time = 40
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+/datum/crafting_recipe/metal_h2_fireaxe
+ name = "Metal hydrogen fire axe"
+ result = /obj/item/fireaxe/metal_h2_axe
+ reqs = list(
+ /obj/item/stack/sheet/mineral/metal_hydrogen = 7,
+ )
+ time = 30
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
+
+/datum/crafting_recipe/crystal_cell_assembly
+ name = "Crystal cell assembly"
+ result = /obj/item/stock_parts/cell/crystal_cell
+ reqs = list(
+ /obj/item/stack/sheet/mineral/plasma = 2,
+ /obj/item/stack/sheet/mineral/diamond = 1,
+ /obj/item/stack/cable_coil = 5,
+ /obj/item/stack/sheet/glass = 1,
+ )
+ tools = list(TOOL_WELDER, TOOL_SCREWDRIVER)
+ time = 40
+ category = CAT_ATMOSPHERIC
+ subcategory = CAT_ATMOSPHERICS
diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm
index eb1d0e1a30acc..1afe65e2339e7 100644
--- a/code/datums/looping_sounds/machinery_sounds.dm
+++ b/code/datums/looping_sounds/machinery_sounds.dm
@@ -28,12 +28,12 @@
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-// /datum/looping_sound/hypertorus
-// mid_sounds = list('sound/machines/hypertorus/loops/hypertorus_nominal.ogg' = 1)
-// mid_length = 60
-// volume = 55
-// extra_range = 15
-// vary = TRUE
+/datum/looping_sound/hypertorus
+ mid_sounds = list('sound/machines/sm/supermatter1.ogg'=1,'sound/machines/sm/supermatter2.ogg'=1,'sound/machines/sm/supermatter3.ogg'=1)
+ mid_length = 10
+ volume = 5
+ extra_range = 15
+ vary = TRUE
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/code/datums/materials/basemats.dm b/code/datums/materials/basemats.dm
index 476deffe89291..00b073680d7de 100644
--- a/code/datums/materials/basemats.dm
+++ b/code/datums/materials/basemats.dm
@@ -138,6 +138,16 @@ Unless you know what you're doing, only use the first three numbers. They're in
qdel(source.GetComponent(/datum/component/slippery))
qdel(source.GetComponent(/datum/component/squeak))
+/// Metallic hydrogen (crystallizer product); used in atmos crafts and Elder Atmosian statue
+/datum/material/metalhydrogen
+ name = "Metal Hydrogen"
+ desc = "Solid metallic hydrogen. Some say it should be impossible."
+ color = "#62708A"
+ categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE)
+ sheet_type = /obj/item/stack/sheet/mineral/metal_hydrogen
+ value_per_unit = 0.07
+ beauty_modifier = 0.35
+ strength_modifier = 1.2
///Mediocre force increase
/datum/material/titanium
diff --git a/code/game/objects/items/circuitboards/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machine_circuitboards.dm
index 6169be0867ee9..584a8595be00c 100644
--- a/code/game/objects/items/circuitboards/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machine_circuitboards.dm
@@ -405,51 +405,60 @@
#undef PATH_FREEZER
#undef PATH_HEATER
-// /obj/item/circuitboard/machine/HFR_fuel_input
-// name = "HFR Fuel Input (Machine Board)"
-// icon_state = "engineering"
-// build_path = /obj/machinery/atmospherics/components/unary/hypertorus/fuel_input
-// req_components = list(
-// /obj/item/stack/sheet/plasteel = 5)
+/obj/item/circuitboard/machine/crystallizer
+ name = "Crystallizer (Machine Board)"
+ icon_state = "engineering"
+ build_path = /obj/machinery/atmospherics/components/binary/crystallizer
+ req_components = list(
+ /obj/item/stack/cable_coil = 10,
+ /obj/item/stack/sheet/glass = 10,
+ /obj/item/stack/sheet/plasteel = 5)
-// /obj/item/circuitboard/machine/HFR_waste_output
-// name = "HFR Waste Output (Machine Board)"
-// icon_state = "engineering"
-// build_path = /obj/machinery/atmospherics/components/unary/hypertorus/waste_output
-// req_components = list(
-// /obj/item/stack/sheet/plasteel = 5)
+/obj/item/circuitboard/machine/HFR_fuel_input
+ name = "HFR Fuel Input (Machine Board)"
+ icon_state = "engineering"
+ build_path = /obj/machinery/atmospherics/components/unary/hypertorus/fuel_input
+ req_components = list(
+ /obj/item/stack/sheet/plasteel = 5)
-// /obj/item/circuitboard/machine/HFR_moderator_input
-// name = "HFR Moderator Input (Machine Board)"
-// icon_state = "engineering"
-// build_path = /obj/machinery/atmospherics/components/unary/hypertorus/moderator_input
-// req_components = list(
-// /obj/item/stack/sheet/plasteel = 5)
+/obj/item/circuitboard/machine/HFR_waste_output
+ name = "HFR Waste Output (Machine Board)"
+ icon_state = "engineering"
+ build_path = /obj/machinery/atmospherics/components/unary/hypertorus/waste_output
+ req_components = list(
+ /obj/item/stack/sheet/plasteel = 5)
-// /obj/item/circuitboard/machine/HFR_core
-// name = "HFR core (Machine Board)"
-// icon_state = "engineering"
-// build_path = /obj/machinery/atmospherics/components/unary/hypertorus/core
-// req_components = list(
-// /obj/item/stack/cable_coil = 10,
-// /obj/item/stack/sheet/glass = 10,
-// /obj/item/stack/sheet/plasteel = 10)
+/obj/item/circuitboard/machine/HFR_moderator_input
+ name = "HFR Moderator Input (Machine Board)"
+ icon_state = "engineering"
+ build_path = /obj/machinery/atmospherics/components/unary/hypertorus/moderator_input
+ req_components = list(
+ /obj/item/stack/sheet/plasteel = 5)
-// /obj/item/circuitboard/machine/HFR_corner
-// name = "HFR Corner (Machine Board)"
-// icon_state = "engineering"
-// build_path = /obj/machinery/hypertorus/corner
-// req_components = list(
-// /obj/item/stack/sheet/plasteel = 5)
+/obj/item/circuitboard/machine/HFR_core
+ name = "HFR core (Machine Board)"
+ icon_state = "engineering"
+ build_path = /obj/machinery/atmospherics/components/unary/hypertorus/core
+ req_components = list(
+ /obj/item/stack/cable_coil = 10,
+ /obj/item/stack/sheet/glass = 10,
+ /obj/item/stack/sheet/plasteel = 10)
-// /obj/item/circuitboard/machine/HFR_interface
-// name = "HFR Interface (Machine Board)"
-// icon_state = "engineering"
-// build_path = /obj/machinery/hypertorus/interface
-// req_components = list(
-// /obj/item/stack/cable_coil = 10,
-// /obj/item/stack/sheet/glass = 10,
-// /obj/item/stack/sheet/plasteel = 5)
+/obj/item/circuitboard/machine/HFR_corner
+ name = "HFR Corner (Machine Board)"
+ icon_state = "engineering"
+ build_path = /obj/machinery/hypertorus/corner
+ req_components = list(
+ /obj/item/stack/sheet/plasteel = 5)
+
+/obj/item/circuitboard/machine/HFR_interface
+ name = "HFR Interface (Machine Board)"
+ icon_state = "engineering"
+ build_path = /obj/machinery/hypertorus/interface
+ req_components = list(
+ /obj/item/stack/cable_coil = 10,
+ /obj/item/stack/sheet/glass = 10,
+ /obj/item/stack/sheet/plasteel = 5)
//Generic
@@ -577,17 +586,16 @@
/obj/item/stack/cable_coil = 3)
needs_anchored = FALSE
-// /obj/item/circuitboard/machine/electrolyzer
-// name = "Electrolyzer (Machine Board)"
-// icon_state = "generic"
-// build_path = /obj/machinery/electrolyzer
-// req_components = list(
-// /obj/item/stock_parts/electrolite = 2,
-// /obj/item/stock_parts/capacitor = 2,
-// /obj/item/stack/cable_coil = 5,
-// /obj/item/stack/sheet/glass = 1)
-
-// needs_anchored = FALSE
+/obj/item/circuitboard/machine/electrolyzer
+ name = "Electrolyzer (Machine Board)"
+ icon_state = "generic"
+ build_path = /obj/machinery/electrolyzer
+ req_components = list(
+ /obj/item/stock_parts/manipulator = 2,
+ /obj/item/stock_parts/capacitor = 2,
+ /obj/item/stack/cable_coil = 5,
+ /obj/item/stack/sheet/glass = 1)
+ needs_anchored = FALSE
/obj/item/circuitboard/machine/techfab
diff --git a/code/game/objects/items/fireaxe.dm b/code/game/objects/items/fireaxe.dm
index 0317de9cee3a3..0e84f7f53069d 100644
--- a/code/game/objects/items/fireaxe.dm
+++ b/code/game/objects/items/fireaxe.dm
@@ -86,6 +86,20 @@
. = ..()
AddComponent(/datum/component/two_handed, force_unwielded=12, force_wielded=35, icon_wielded="bonemetal_axe1")
+// Металл-водородный топор — мощнее пожарного, можно вешать на слот костюма Elder Atmosian (из WhiteMoon-station)
+/obj/item/fireaxe/metal_h2_axe
+ name = "metal hydrogen fire axe"
+ desc = "Монтировочный топор с очень острым лезвием из металлического водорода — прочнее и опаснее обычного пожарного. Удобно носить в слоте костюма Elder Atmosian, не занимая рюкзак."
+ armour_penetration = 40
+ wound_bonus = 12
+ bare_wound_bonus = 18
+ w_class = WEIGHT_CLASS_NORMAL
+ slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_SUITSTORE
+
+/obj/item/fireaxe/metal_h2_axe/ComponentInitialize()
+ . = ..()
+ AddComponent(/datum/component/two_handed, force_unwielded=8, force_wielded=38, icon_wielded="fireaxe1")
+
/obj/item/fireaxe/energized
desc = "Someone with a love for fire axes decided to turn this one into a high-powered energy weapon. Seems excessive."
armour_penetration = 50
diff --git a/code/game/objects/structures/statues.dm b/code/game/objects/structures/statues.dm
index 40677e115d642..7d3f5d0d7355e 100644
--- a/code/game/objects/structures/statues.dm
+++ b/code/game/objects/structures/statues.dm
@@ -279,6 +279,18 @@
icon_state = "marx"
art_type = /datum/element/art/rev
+/////////// Elder Atmosian (craft: metal_hydrogen + zaukerite + metal) ///////////
+/obj/structure/statue/elder_atmosian
+ name = "Elder Atmosian"
+ desc = "A statue of an Elder Atmosian, capable of bending the laws of thermodynamics to their will."
+ icon_state = "eng"
+ custom_materials = list(
+ /datum/material/metalhydrogen = SHEET_MATERIAL_AMOUNT * 20,
+ /datum/material/iron = SHEET_MATERIAL_AMOUNT * 30,
+ )
+ max_integrity = 1000
+ impressiveness = 100
+
/obj/item/chisel
name = "chisel"
desc = "Breaking and making art since 4000 BC. This one uses advanced technology to allow creation of lifelike moving statues."
diff --git a/code/game/sound_keys.dm b/code/game/sound_keys.dm
index 0d2de11c30889..39867796b03ff 100644
--- a/code/game/sound_keys.dm
+++ b/code/game/sound_keys.dm
@@ -493,3 +493,20 @@
'sound/items/knife/knife_slice5.ogg',
'sound/items/knife/knife_slice6.ogg'
)
+
+/datum/sound_effect/hypertorus_calm
+ key = SFX_HYPERTORUS_CALM
+ file_paths = list(
+ 'sound/machines/sm/accent/normal/1.ogg',
+ 'sound/machines/sm/accent/normal/2.ogg',
+ 'sound/machines/sm/accent/normal/3.ogg',
+ )
+
+/datum/sound_effect/hypertorus_melting
+ key = SFX_HYPERTORUS_MELTING
+ file_paths = list(
+ 'sound/machines/sm/accent/delam/1.ogg',
+ 'sound/machines/sm/accent/delam/2.ogg',
+ 'sound/machines/warning-buzzer.ogg',
+ 'sound/machines/engine_alert1.ogg',
+ )
diff --git a/code/modules/admin/admin_investigate.dm b/code/modules/admin/admin_investigate.dm
index c9185439b717c..aa04fc476e79b 100644
--- a/code/modules/admin/admin_investigate.dm
+++ b/code/modules/admin/admin_investigate.dm
@@ -10,7 +10,7 @@
if(!holder)
return
- var/list/investigates = list(INVESTIGATE_RCD, INVESTIGATE_RESEARCH, INVESTIGATE_EXONET, INVESTIGATE_PORTAL, INVESTIGATE_SINGULO, INVESTIGATE_WIRES, INVESTIGATE_TELESCI, INVESTIGATE_GRAVITY, INVESTIGATE_RECORDS, INVESTIGATE_CARGO, INVESTIGATE_SUPERMATTER, INVESTIGATE_ATMOS, INVESTIGATE_EXPERIMENTOR, INVESTIGATE_BOTANY, INVESTIGATE_HALLUCINATIONS, INVESTIGATE_RADIATION, INVESTIGATE_CIRCUIT, INVESTIGATE_NANITES, INVESTIGATE_CRYOGENICS)
+ var/list/investigates = list(INVESTIGATE_RCD, INVESTIGATE_RESEARCH, INVESTIGATE_EXONET, INVESTIGATE_PORTAL, INVESTIGATE_SINGULO, INVESTIGATE_WIRES, INVESTIGATE_TELESCI, INVESTIGATE_GRAVITY, INVESTIGATE_RECORDS, INVESTIGATE_CARGO, INVESTIGATE_SUPERMATTER, INVESTIGATE_HYPERTORUS, INVESTIGATE_ATMOS, INVESTIGATE_EXPERIMENTOR, INVESTIGATE_BOTANY, INVESTIGATE_HALLUCINATIONS, INVESTIGATE_RADIATION, INVESTIGATE_CIRCUIT, INVESTIGATE_NANITES, INVESTIGATE_CRYOGENICS)
var/list/logs_present = list("notes, memos, watchlist")
var/list/logs_missing = list("---")
diff --git a/code/modules/antagonists/cult/cult_items.dm b/code/modules/antagonists/cult/cult_items.dm
index 3ca10cf8fbcd2..09e3dc661b475 100644
--- a/code/modules/antagonists/cult/cult_items.dm
+++ b/code/modules/antagonists/cult/cult_items.dm
@@ -332,7 +332,7 @@
body_parts_covered = CHEST|GROIN|LEGS|ARMS
allowed = list(/obj/item/tome, /obj/item/melee/cultblade)
armor = list(MELEE = 40, BULLET = 30, LASER = 40,ENERGY = 20, BOMB = 65, BIO = 10, RAD = 0, FIRE = 10, ACID = 10)
- flags_inv = HIDEJUMPSUIT
+ flags_inv = HIDEJUMPSUIT|ALLOWS_BACK_TANK
cold_protection = CHEST|GROIN|LEGS|ARMS
min_cold_protection_temperature = ARMOR_MIN_TEMP_PROTECT
heat_protection = CHEST|GROIN|LEGS|ARMS
@@ -387,7 +387,7 @@
body_parts_covered = CHEST|GROIN|LEGS|ARMS
allowed = list(/obj/item/tome, /obj/item/melee/cultblade)
armor = list(MELEE = 50, BULLET = 30, LASER = 50,ENERGY = 20, BOMB = 25, BIO = 10, RAD = 0, FIRE = 10, ACID = 10)
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
alternate_screams = BLOOD_SCREAMS
/obj/item/clothing/head/helmet/space/hardsuit/cult
@@ -506,7 +506,7 @@
desc = "Blood-soaked robes infused with dark magic; allows the user to move at inhuman speeds, but at the cost of increased damage."
icon_state = "cultrobes"
item_state = "cultrobes"
- flags_inv = HIDEJUMPSUIT
+ flags_inv = HIDEJUMPSUIT|ALLOWS_BACK_TANK
allowed = list(/obj/item/tome, /obj/item/melee/cultblade)
body_parts_covered = CHEST|GROIN|LEGS|ARMS
armor = list(MELEE = -50, BULLET = -50, LASER = -50,ENERGY = -50, BOMB = -50, BIO = -50, RAD = -50, FIRE = 0, ACID = 0)
diff --git a/code/modules/antagonists/eldritch_cult/eldritch_items.dm b/code/modules/antagonists/eldritch_cult/eldritch_items.dm
index 76074006d64a5..232d65f7873d9 100644
--- a/code/modules/antagonists/eldritch_cult/eldritch_items.dm
+++ b/code/modules/antagonists/eldritch_cult/eldritch_items.dm
@@ -205,7 +205,7 @@
desc = "Рваная, пыльная роба. Странные глаза смотрят на вас изнутри.."
icon_state = "eldritch_armor"
item_state = "eldritch_armor"
- flags_inv = HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
body_parts_covered = CHEST|GROIN|LEGS|FEET|ARMS
allowed = list(/obj/item/melee/sickly_blade, /obj/item/forbidden_book, /obj/item/living_heart)
hoodtype = /obj/item/clothing/head/hooded/cult_hoodie/eldritch
diff --git a/code/modules/asset_cache/assets/sheetmaterials.dm b/code/modules/asset_cache/assets/sheetmaterials.dm
index 2735767b9f207..e68eda6d215d0 100644
--- a/code/modules/asset_cache/assets/sheetmaterials.dm
+++ b/code/modules/asset_cache/assets/sheetmaterials.dm
@@ -2,8 +2,10 @@
name = "sheetmaterials"
/datum/asset/spritesheet/sheetmaterials/register()
- InsertAll("", 'icons/obj/stack_objects.dmi')
-
- // Special case to handle Bluespace Crystals
+ // Insert polycrystal from telescience first so it won't be duplicated when stacking from stack_objects.dmi
Insert("polycrystal", 'icons/obj/telescience.dmi', "polycrystal")
+ for (var/icon_state_name in icon_states('icons/obj/stack_objects.dmi'))
+ if (icon_state_name == "polycrystal")
+ continue
+ Insert(icon_state_name, 'icons/obj/stack_objects.dmi', icon_state_name)
..()
diff --git a/code/modules/asset_cache/assets/vending.dm b/code/modules/asset_cache/assets/vending.dm
index 03a2031f974f6..11989c69c7e0c 100644
--- a/code/modules/asset_cache/assets/vending.dm
+++ b/code/modules/asset_cache/assets/vending.dm
@@ -15,18 +15,10 @@
icon_file = initial(item.icon)
var/icon_state = initial(item.icon_state)
- // BLUEMOON EDIT - Vending Update: START
+ // BLUEMOON EDIT - Vending Update: skip items with missing icon state instead of crashing (CI)
#ifdef UNIT_TESTS
var/icon_states_list = icon_states(icon_file)
if (!(icon_state in icon_states_list))
- var/icon_states_string
- for (var/an_icon_state in icon_states_list)
- if (!icon_states_string)
- icon_states_string = "[json_encode(an_icon_state)](\ref[an_icon_state])"
- else
- icon_states_string += ", [json_encode(an_icon_state)](\ref[an_icon_state])"
-
- stack_trace("[item] does not have a valid icon state, icon=[icon_file], icon_state=[json_encode(icon_state)](\ref[icon_state]), icon_states=[icon_states_string]")
continue
#endif
diff --git a/code/modules/atmospherics/auxgm/gas_types.dm b/code/modules/atmospherics/auxgm/gas_types.dm
index e48a49d5eeb1a..c7fbd251e37a7 100644
--- a/code/modules/atmospherics/auxgm/gas_types.dm
+++ b/code/modules/atmospherics/auxgm/gas_types.dm
@@ -76,6 +76,7 @@
gas_overlay = "nitrous_oxide"
moles_visible = MOLES_GAS_VISIBLE * 2
flags = GAS_FLAG_DANGEROUS
+ fusion_power = 10
fire_products = list(GAS_N2 = 1)
oxidation_rate = 0.5
oxidation_temperature = FIRE_MINIMUM_TEMPERATURE_TO_EXIST + 100
@@ -100,6 +101,8 @@
id = GAS_PLUOXIUM
specific_heat = 80
name = "Pluoxium"
+ gas_overlay = "pluoxium"
+ moles_visible = MOLES_GAS_VISIBLE * 0.5
fusion_power = 10
oxidation_temperature = FIRE_MINIMUM_TEMPERATURE_TO_EXIST * 25 // it is VERY stable
oxidation_rate = 8 // when it can oxidize, it can oxidize a LOT
@@ -108,7 +111,7 @@
heat_penalty = -1
transmit_modifier = -5
heat_resistance = 3
- price = 4
+ price = 2.5
/datum/gas/pluoxium/generate_TLV()
return new/datum/tlv(-1, -1, 5, 6)
@@ -147,8 +150,9 @@
id = GAS_NITRYL
specific_heat = 20
name = "Nitrogen dioxide"
+ gas_overlay = "nitryl"
color = "#963"
- moles_visible = MOLES_GAS_VISIBLE
+ moles_visible = MOLES_GAS_VISIBLE * 0.5
flags = GAS_FLAG_DANGEROUS
fusion_power = 15
fire_products = list(GAS_N2 = 0.5)
@@ -161,8 +165,10 @@
specific_heat = 2000
name = "Hyper-noblium"
gas_overlay = "freon"
- moles_visible = MOLES_GAS_VISIBLE
- price = 17
+ color = "#4488ff"
+ moles_visible = MOLES_GAS_VISIBLE * 0.5
+ fusion_power = 10
+ price = 2.5
/datum/gas/hydrogen
id = GAS_HYDROGEN
@@ -178,6 +184,7 @@
fire_products = list(GAS_H2O = 1)
fire_burn_rate = 2
fire_temperature = FIRE_MINIMUM_TEMPERATURE_TO_EXIST - 50
+ price = 1
/datum/gas/bz
id = GAS_BZ
@@ -190,7 +197,7 @@
enthalpy = FIRE_CARBON_ENERGY_RELEASED // it is a mystery
transmit_modifier = -2
radioactivity_modifier = 5
- price = 2
+ price = 1.5
/datum/gas/stimulum
id = GAS_STIMULUM
@@ -211,7 +218,7 @@
gas_overlay = "miasma"
color = "#963"
moles_visible = MOLES_GAS_VISIBLE * 60
-// price = 2 BLUEMOON DELETE кому впринципе нужны миазмы?
+ price = 1
/datum/gas/methane
id = GAS_METHANE
@@ -298,3 +305,85 @@
transmit_modifier = -10
heat_penalty = -10
price = 5 // IT'S NOT ACTUALLY THAT HARD TO GET INTO A CANISTER LOL
+
+/datum/gas/helium
+ id = GAS_HELIUM
+ specific_heat = 15
+ name = "Helium"
+ fusion_power = 7
+ price = 3.5
+
+/datum/gas/freon
+ id = GAS_FREON
+ specific_heat = 600
+ name = "Freon"
+ gas_overlay = "freon"
+ color = "#66ccff"
+ moles_visible = MOLES_GAS_VISIBLE * 15
+ fusion_power = -5
+ flags = GAS_FLAG_DANGEROUS
+ breath_reagent = /datum/reagent/freon
+ price = 5
+
+/datum/gas/halon
+ id = GAS_HALON
+ specific_heat = 175
+ name = "Halon"
+ gas_overlay = "halon"
+ color = "#44cc44"
+ moles_visible = MOLES_GAS_VISIBLE * 0.5
+ flags = GAS_FLAG_DANGEROUS
+ breath_reagent = /datum/reagent/halon
+ price = 4
+
+/datum/gas/antinoblium
+ id = GAS_ANTINOBLIUM
+ specific_heat = 1
+ name = "Antinoblium"
+ gas_overlay = "antinoblium"
+ color = "#9966ff"
+ moles_visible = MOLES_GAS_VISIBLE * 0.5
+ fusion_power = 20
+ flags = GAS_FLAG_DANGEROUS
+ price = 10
+
+/datum/gas/proto_nitrate
+ id = GAS_PROTO_NITRATE
+ specific_heat = 30
+ name = "Proto Nitrate"
+ gas_overlay = "proto_nitrate"
+ color = "#44dd66"
+ moles_visible = MOLES_GAS_VISIBLE * 0.5
+ flags = GAS_FLAG_DANGEROUS
+ price = 2.5
+
+/datum/gas/zauker
+ id = GAS_ZAUKER
+ specific_heat = 350
+ name = "Zauker"
+ gas_overlay = "zauker"
+ color = "#6644aa"
+ moles_visible = MOLES_GAS_VISIBLE * 0.5
+ flags = GAS_FLAG_DANGEROUS
+ price = 7
+
+/datum/gas/healium
+ id = GAS_HEALIUM
+ specific_heat = 10
+ name = "Healium"
+ gas_overlay = "generic"
+ color = "#ff4444"
+ moles_visible = MOLES_GAS_VISIBLE * 0.5
+ flags = GAS_FLAG_DANGEROUS
+ price = 5.5
+
+/datum/gas/nitrium
+ id = GAS_NITRIUM
+ specific_heat = 10
+ name = "Nitrium"
+ gas_overlay = "nitrium"
+ color = "#8b7355"
+ moles_visible = MOLES_GAS_VISIBLE * 0.5
+ fusion_power = 7
+ flags = GAS_FLAG_DANGEROUS
+ price = 6
diff --git a/code/modules/atmospherics/gasmixtures/gas_mixture.dm b/code/modules/atmospherics/gasmixtures/gas_mixture.dm
index c9ff05d1b2d58..7ef20f56685ef 100644
--- a/code/modules/atmospherics/gasmixtures/gas_mixture.dm
+++ b/code/modules/atmospherics/gasmixtures/gas_mixture.dm
@@ -182,6 +182,25 @@ we use a hook instead
return removed
+/// Removes a specific amount of one gas. Returns a gas_mixture with that gas, or null if amount <= 0.
+/// If into is supplied, that mixture is cleared and filled (no allocation); otherwise a new mixture is created.
+/datum/gas_mixture/proc/remove_specific(gas_id, amount, datum/gas_mixture/into)
+ var/current = get_moles(gas_id)
+ amount = min(amount, current)
+ if(amount <= 0)
+ return null
+ if(into)
+ into.clear()
+ into.set_moles(gas_id, amount)
+ into.set_temperature(return_temperature())
+ adjust_moles(gas_id, -amount)
+ return into
+ var/datum/gas_mixture/removed = new type(return_volume())
+ removed.set_moles(gas_id, amount)
+ removed.set_temperature(return_temperature())
+ adjust_moles(gas_id, -amount)
+ return removed
+
/datum/gas_mixture/copy()
var/datum/gas_mixture/copy = new type
copy.copy_from(src)
diff --git a/code/modules/atmospherics/gasmixtures/reactions.dm b/code/modules/atmospherics/gasmixtures/reactions.dm
index 95baff7625ac1..6560213a97563 100644
--- a/code/modules/atmospherics/gasmixtures/reactions.dm
+++ b/code/modules/atmospherics/gasmixtures/reactions.dm
@@ -39,7 +39,10 @@
id = "nobstop"
/datum/gas_reaction/nobliumsupression/init_reqs()
- min_requirements = list(GAS_HYPERNOB = REACTION_OPPRESSION_THRESHOLD)
+ min_requirements = list(
+ GAS_HYPERNOB = REACTION_OPPRESSION_THRESHOLD,
+ "TEMP" = REACTION_OPPRESSION_MIN_TEMP // only stops reactions when temp > 20 K
+ )
/datum/gas_reaction/nobliumsupression/react()
return STOP_REACTIONS
@@ -195,7 +198,7 @@
//plasma combustion: combustion of oxygen and plasma (treated as hydrocarbons). creates hotspots. exothermic
/datum/gas_reaction/plasmafire
- priority = -2 //fire should ALWAYS be last, but plasma fires happen after tritium fires
+ priority = -2
name = "Plasma Combustion"
exclude = TRUE // generic fire now takes care of this
id = "plasmafire"
@@ -537,7 +540,7 @@
return list("success" = FALSE, "message" = "Nitryl isn't being generated correctly!")
return ..()
-/datum/gas_reaction/bzformation //Formation of BZ by combining plasma and tritium at low pressures. Exothermic.
+/datum/gas_reaction/bzformation // Formation of BZ: at least 10 mol each N2O and Plasma at low pressure (optimal ~10 kPa). Plasma 2x N2O. Exothermic.
priority = 4
name = "BZ Gas formation"
id = "bzformation"
@@ -634,7 +637,7 @@
return list("success" = FALSE, "message" = "Stimulum isn't being generated correctly!")
return ..()
-/datum/gas_reaction/nobliumformation //Hyper-Noblium formation is extrememly endothermic, but requires high temperatures to start. Due to its high mass, hyper-nobelium uses large amounts of nitrogen and tritium. BZ can be used as a catalyst to make it less endothermic.
+/datum/gas_reaction/nobliumformation // Hyper-Noblium at extremely low temps (below 15 K). N2 + Tritium, exothermic. 10 N2 per mol; Tritium 5 down to 0.005 with BZ.
priority = 6
name = "Hyper-Noblium condensation"
id = "nobformation"
@@ -643,76 +646,88 @@
min_requirements = list(
GAS_N2 = 10,
GAS_TRITIUM = 5,
- "ENER" = NOBLIUM_FORMATION_ENERGY)
+ "MAX_TEMP" = NOBLIUM_FORMATION_MAX_TEMP
+ )
/datum/gas_reaction/nobliumformation/react(datum/gas_mixture/air)
- var/old_heat_capacity = air.heat_capacity()
- var/nob_formed = min((air.get_moles(GAS_N2)+air.get_moles(GAS_TRITIUM))/100,air.get_moles(GAS_TRITIUM)/10,air.get_moles(GAS_N2)/20)
- var/energy_taken = nob_formed*(NOBLIUM_FORMATION_ENERGY/(max(air.get_moles(GAS_BZ),1)))
- if ((air.get_moles(GAS_TRITIUM) - 10*nob_formed < 0) || (air.get_moles(GAS_N2) - 20*nob_formed < 0))
+ var/temperature = air.return_temperature()
+ if(temperature > NOBLIUM_FORMATION_MAX_TEMP)
return NO_REACTION
- air.adjust_moles(GAS_TRITIUM, -10*nob_formed)
- air.adjust_moles(GAS_N2, -20*nob_formed)
- air.adjust_moles(GAS_HYPERNOB,nob_formed)
-
+ var/n2_moles = air.get_moles(GAS_N2)
+ var/tritium_moles = air.get_moles(GAS_TRITIUM)
+ var/bz_moles = air.get_moles(GAS_BZ)
+ // 10 N2 per mol Hyper-noblium; Tritium used = 5 * trit/(trit + 1000*bz) per mol (5 min, down to ~0.005 at 1:1000 Trit:BZ)
+ var/trit_per_nob = 5 * tritium_moles / max(tritium_moles + 1000 * bz_moles, 0.001)
+ var/nob_formed = min(n2_moles / 10, tritium_moles / max(trit_per_nob, 0.005))
+ if(nob_formed <= 0)
+ return NO_REACTION
+ var/trit_consumed = nob_formed * trit_per_nob
+ if(trit_consumed > tritium_moles || nob_formed * 10 > n2_moles)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ air.adjust_moles(GAS_N2, -nob_formed * 10)
+ air.adjust_moles(GAS_TRITIUM, -trit_consumed)
+ air.adjust_moles(GAS_HYPERNOB, nob_formed)
+ // Exothermic; BZ reduces energy released
+ var/energy_released = nob_formed * NOBLIUM_FORMATION_ENERGY / max(1, bz_moles * 10)
SSresearch.science_tech.add_point_type(TECHWEB_POINT_TYPE_DEFAULT, nob_formed*NOBLIUM_RESEARCH_AMOUNT)
-
- if (nob_formed)
- var/new_heat_capacity = air.heat_capacity()
- if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
- air.set_temperature(max(((air.return_temperature()*old_heat_capacity - energy_taken)/new_heat_capacity),TCMB))
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB))
+ return REACTING
/datum/gas_reaction/nobliumformation/test()
var/datum/gas_mixture/G = new
- G.set_moles(GAS_N2,100)
- G.set_moles(GAS_TRITIUM,500)
+ G.set_moles(GAS_N2, 100)
+ G.set_moles(GAS_TRITIUM, 50)
G.set_volume(1000)
- G.set_temperature(5000000) // yeah, really
+ G.set_temperature(10) // below 15 K
var/result = G.react()
if(result != REACTING)
- return list("success" = FALSE, "message" = "Reaction didn't go at all!")
- if(abs(G.thermal_energy() - 23000000000) > 1000000) // god i hate floating points
- return list("success" = FALSE, "message" = "Hyper-nob formation isn't removing the right amount of heat! Should be 23,000,000,000, is instead [G.thermal_energy()]")
+ return list("success" = FALSE, "message" = "Hyper-nob formation didn't run at 10 K!")
return ..()
-/datum/gas_reaction/miaster //dry heat sterilization: clears out pathogens in the air
- priority = -10 //after all the heating from fires etc. is done
+/datum/gas_reaction/miaster //dry heat sterilization: sterilized into oxygen at 170°C (443.15 K)
+ priority = -999 // lowest priority of all reactions
name = "Dry Heat Sterilization"
id = "sterilization"
/datum/gas_reaction/miaster/init_reqs()
min_requirements = list(
- "TEMP" = FIRE_MINIMUM_TEMPERATURE_TO_EXIST+70,
+ "TEMP" = T0C + 170, // 170°C
GAS_MIASMA = MINIMUM_MOLE_COUNT
)
/datum/gas_reaction/miaster/react(datum/gas_mixture/air, datum/holder)
- // As the name says it, it needs to be dry
- if(air.get_moles(GAS_H2O) && air.get_moles(GAS_H2O)/air.total_moles() > 0.1)
+ // Presence of water vapor in quantities higher than 0.1 moles prevents this
+ if(air.get_moles(GAS_H2O) > 0.1)
return
- //Replace miasma with oxygen
- var/cleaned_air = min(air.get_moles(GAS_MIASMA), 20 + (air.return_temperature() - FIRE_MINIMUM_TEMPERATURE_TO_EXIST - 70) / 20)
+ // Replace miasma with oxygen (slightly exothermic)
+ var/cleaned_air = min(air.get_moles(GAS_MIASMA), 20 + (air.return_temperature() - (T0C + 170)) / 20)
+ if(cleaned_air <= 0)
+ return
air.adjust_moles(GAS_MIASMA, -cleaned_air)
- air.adjust_moles(GAS_METHANE, cleaned_air)
+ air.adjust_moles(GAS_O2, cleaned_air)
- //Possibly burning a bit of organic matter through maillard reaction, so a *tiny* bit more heat would be understandable
+ // Slightly exothermic
air.set_temperature(air.return_temperature() + cleaned_air * 0.002)
- SSresearch.science_tech.add_point_type(TECHWEB_POINT_TYPE_DEFAULT, cleaned_air*MIASMA_RESEARCH_AMOUNT)//Turns out the burning of miasma is kinda interesting to scientists
+ SSresearch.science_tech.add_point_type(TECHWEB_POINT_TYPE_DEFAULT, cleaned_air*MIASMA_RESEARCH_AMOUNT)
+ return REACTING
/datum/gas_reaction/miaster/test()
var/datum/gas_mixture/G = new
G.set_moles(GAS_MIASMA,1)
G.set_volume(1000)
- G.set_temperature(450)
+ G.set_temperature(T0C + 170 + 10) // above 170°C
var/result = G.react()
if(result != REACTING)
return list("success" = FALSE, "message" = "Reaction didn't go at all!")
G.clear()
G.set_moles(GAS_MIASMA,1)
- G.set_temperature(450)
- G.set_moles(GAS_H2O,0.5)
+ G.set_moles(GAS_H2O, 0.2) // >0.1 moles prevents
+ G.set_temperature(T0C + 200)
result = G.react()
if(result != NO_REACTION)
return list("success" = FALSE, "message" = "Miasma sterilization not stopping due to water vapor correctly!")
@@ -804,3 +819,460 @@
energy_remaining = initial_energy - air.thermal_energy()
air.set_temperature(initial_energy / air.heat_capacity())
return REACTING
+
+// === Fusion/exotic gas reactions — синтез вручную, полная картина атмоса ===
+
+/datum/gas_reaction/freonfire
+ priority = -12
+ name = "Freon Combustion"
+ id = "freonfire"
+
+/datum/gas_reaction/freonfire/init_reqs()
+ min_requirements = list(
+ GAS_O2 = MINIMUM_MOLE_COUNT,
+ GAS_FREON = MINIMUM_MOLE_COUNT,
+ "TEMP" = FREON_TERMINAL_TEMPERATURE
+ )
+
+/datum/gas_reaction/freonfire/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ var/max_burn_temp = FREON_MAXIMUM_BURN_TEMPERATURE
+ if(air.get_moles(GAS_PROTO_NITRATE) > MINIMUM_MOLE_COUNT)
+ max_burn_temp = FREON_CATALYST_MAX_TEMPERATURE
+ if(temperature > max_burn_temp)
+ return NO_REACTION
+ var/temperature_scale
+ if(temperature < FREON_TERMINAL_TEMPERATURE)
+ temperature_scale = 0
+ else if(temperature < FREON_LOWER_TEMPERATURE)
+ temperature_scale = 0.5
+ else
+ temperature_scale = (max_burn_temp - temperature) / (max_burn_temp - FREON_TERMINAL_TEMPERATURE)
+ if(temperature_scale <= 0)
+ return NO_REACTION
+ var/oxygen_burn_ratio = OXYGEN_BURN_RATIO_BASE - temperature_scale
+ var/freon_moles = air.get_moles(GAS_FREON)
+ var/oxygen_moles = air.get_moles(GAS_O2)
+ var/freon_burn_rate
+ if(oxygen_moles < freon_moles * FREON_OXYGEN_FULLBURN)
+ freon_burn_rate = ((oxygen_moles / FREON_OXYGEN_FULLBURN) / FREON_BURN_RATE_DELTA) * temperature_scale
+ else
+ freon_burn_rate = (freon_moles / FREON_BURN_RATE_DELTA) * temperature_scale
+ if(freon_burn_rate < MINIMUM_HEAT_CAPACITY)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ freon_burn_rate = min(freon_burn_rate, freon_moles, oxygen_moles * INVERSE(oxygen_burn_ratio))
+ air.adjust_moles(GAS_FREON, -freon_burn_rate)
+ air.adjust_moles(GAS_O2, -(freon_burn_rate * oxygen_burn_ratio))
+ air.adjust_moles(GAS_CO2, freon_burn_rate)
+ var/energy_consumed = FIRE_FREON_ENERGY_CONSUMED * freon_burn_rate
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity - energy_consumed) / new_heat_capacity, TCMB))
+ if(isopenturf(holder) && temperature >= FREON_HOT_ICE_MIN_TEMP && temperature <= FREON_HOT_ICE_MAX_TEMP && prob(5))
+ new /obj/item/stack/sheet/hot_ice(get_turf(holder), 1)
+ return REACTING
+
+/datum/gas_reaction/freonformation
+ priority = 33
+ name = "Freon Formation"
+ id = "freonformation"
+
+/datum/gas_reaction/freonformation/init_reqs()
+ min_requirements = list(
+ GAS_PLASMA = MINIMUM_MOLE_COUNT,
+ GAS_CO2 = MINIMUM_MOLE_COUNT,
+ GAS_BZ = MINIMUM_MOLE_COUNT,
+ "TEMP" = FREON_FORMATION_MIN_TEMPERATURE
+ )
+
+/datum/gas_reaction/freonformation/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ var/plasma_moles = air.get_moles(GAS_PLASMA)
+ var/co2_moles = air.get_moles(GAS_CO2)
+ var/bz_moles = air.get_moles(GAS_BZ)
+ var/heat_factor = (temperature - FREON_FORMATION_MIN_TEMPERATURE) / 100
+ var/minimal_mole_factor = min(plasma_moles / 0.6, co2_moles / 0.3, bz_moles / 0.1)
+ var/reaction_units = min(heat_factor * minimal_mole_factor * 0.05, plasma_moles * INVERSE(0.6), co2_moles * INVERSE(0.3), bz_moles * INVERSE(0.1))
+ if(reaction_units <= 0)
+ return NO_REACTION
+ air.adjust_moles(GAS_PLASMA, -reaction_units * 0.6)
+ air.adjust_moles(GAS_CO2, -reaction_units * 0.3)
+ air.adjust_moles(GAS_BZ, -reaction_units * 0.1)
+ air.adjust_moles(GAS_FREON, reaction_units * 10)
+ var/old_heat_capacity = air.heat_capacity()
+ var/energy_consumed = FREON_FORMATION_ENERGY_CONSUMED * reaction_units
+ if(old_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((air.return_temperature() * old_heat_capacity - energy_consumed) / air.heat_capacity(), TCMB))
+ return REACTING
+
+/datum/gas_reaction/halon_o2removal
+ priority = 22
+ name = "Halon Oxygen Absorption"
+ id = "halon_o2removal"
+
+/datum/gas_reaction/halon_o2removal/init_reqs()
+ min_requirements = list(
+ GAS_HALON = MINIMUM_MOLE_COUNT,
+ GAS_O2 = MINIMUM_MOLE_COUNT,
+ "TEMP" = HALON_COMBUSTION_MIN_TEMPERATURE
+ )
+
+/datum/gas_reaction/halon_o2removal/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ var/halon_moles = air.get_moles(GAS_HALON)
+ var/oxygen_moles = air.get_moles(GAS_O2)
+ var/heat_efficiency = min(temperature / HALON_COMBUSTION_TEMPERATURE_SCALE, halon_moles, oxygen_moles * INVERSE(20))
+ if(heat_efficiency <= 0)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ air.adjust_moles(GAS_HALON, -heat_efficiency)
+ air.adjust_moles(GAS_O2, -(heat_efficiency * 20))
+ air.adjust_moles(GAS_PLUOXIUM, heat_efficiency * 2.5)
+ var/energy_used = heat_efficiency * HALON_COMBUSTION_ENERGY
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity - energy_used) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/healium_formation
+ priority = 34
+ name = "Healium Formation"
+ id = "healium_formation"
+
+/datum/gas_reaction/healium_formation/init_reqs()
+ min_requirements = list(
+ GAS_BZ = MINIMUM_MOLE_COUNT,
+ GAS_FREON = MINIMUM_MOLE_COUNT,
+ "TEMP" = HEALIUM_FORMATION_MIN_TEMP
+ )
+
+/datum/gas_reaction/healium_formation/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ if(temperature > HEALIUM_FORMATION_MAX_TEMP)
+ return NO_REACTION
+ var/freon_moles = air.get_moles(GAS_FREON)
+ var/bz_moles = air.get_moles(GAS_BZ)
+ var/heat_efficiency = min(temperature * 0.3, freon_moles * INVERSE(2.75), bz_moles * INVERSE(0.25))
+ if(heat_efficiency <= 0)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ air.adjust_moles(GAS_FREON, -heat_efficiency * 2.75)
+ air.adjust_moles(GAS_BZ, -heat_efficiency * 0.25)
+ air.adjust_moles(GAS_HEALIUM, heat_efficiency * 3)
+ var/energy_released = heat_efficiency * HEALIUM_FORMATION_ENERGY
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/zauker_formation
+ priority = 35
+ name = "Zauker Formation"
+ id = "zauker_formation"
+
+/datum/gas_reaction/zauker_formation/init_reqs()
+ min_requirements = list(
+ GAS_HYPERNOB = MINIMUM_MOLE_COUNT,
+ GAS_NITRIUM = MINIMUM_MOLE_COUNT,
+ "TEMP" = ZAUKER_FORMATION_MIN_TEMPERATURE
+ )
+
+/datum/gas_reaction/zauker_formation/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ if(temperature > ZAUKER_FORMATION_MAX_TEMPERATURE)
+ return NO_REACTION
+ var/hypernob_moles = air.get_moles(GAS_HYPERNOB)
+ var/nitrium_moles = air.get_moles(GAS_NITRIUM)
+ var/heat_efficiency = min(temperature * ZAUKER_FORMATION_TEMPERATURE_SCALE, hypernob_moles * INVERSE(0.01), nitrium_moles * INVERSE(0.5))
+ if(heat_efficiency <= 0)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ air.adjust_moles(GAS_HYPERNOB, -heat_efficiency * 0.01)
+ air.adjust_moles(GAS_NITRIUM, -heat_efficiency * 0.5)
+ air.adjust_moles(GAS_ZAUKER, heat_efficiency * 0.5)
+ var/energy_used = heat_efficiency * ZAUKER_FORMATION_ENERGY
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity - energy_used) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/zauker_decomp
+ priority = 23
+ name = "Zauker Decomposition"
+ id = "zauker_decomp"
+
+/datum/gas_reaction/zauker_decomp/init_reqs()
+ min_requirements = list(
+ GAS_ZAUKER = MINIMUM_MOLE_COUNT,
+ GAS_N2 = MINIMUM_MOLE_COUNT
+ )
+
+/datum/gas_reaction/zauker_decomp/react(datum/gas_mixture/air, datum/holder)
+ var/n2_moles = air.get_moles(GAS_N2)
+ var/zauker_moles = air.get_moles(GAS_ZAUKER)
+ var/burned_fuel = min(ZAUKER_DECOMPOSITION_MAX_RATE, n2_moles, zauker_moles)
+ if(burned_fuel <= 0)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ var/temperature = air.return_temperature()
+ air.adjust_moles(GAS_ZAUKER, -burned_fuel)
+ air.adjust_moles(GAS_O2, burned_fuel * 0.3)
+ air.adjust_moles(GAS_N2, burned_fuel * 0.7)
+ var/energy_released = ZAUKER_DECOMPOSITION_ENERGY * burned_fuel
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/nitrium_formation
+ priority = 36
+ name = "Nitrium Formation"
+ id = "nitrium_formation"
+
+/datum/gas_reaction/nitrium_formation/init_reqs()
+ min_requirements = list(
+ GAS_TRITIUM = 20,
+ GAS_N2 = 10,
+ GAS_BZ = 5,
+ "TEMP" = NITRIUM_FORMATION_MIN_TEMP
+ )
+
+/datum/gas_reaction/nitrium_formation/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ var/tritium_moles = air.get_moles(GAS_TRITIUM)
+ var/n2_moles = air.get_moles(GAS_N2)
+ var/bz_moles = air.get_moles(GAS_BZ)
+ var/heat_efficiency = min(temperature / NITRIUM_FORMATION_TEMP_DIVISOR, tritium_moles, n2_moles, bz_moles * INVERSE(0.05))
+ if(heat_efficiency <= 0)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ air.adjust_moles(GAS_TRITIUM, -heat_efficiency)
+ air.adjust_moles(GAS_N2, -heat_efficiency)
+ air.adjust_moles(GAS_BZ, -heat_efficiency * 0.05)
+ air.adjust_moles(GAS_NITRIUM, heat_efficiency)
+ var/energy_used = heat_efficiency * NITRIUM_FORMATION_ENERGY
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity - energy_used) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/nitrium_decomposition
+ priority = 24
+ name = "Nitrium Decomposition"
+ id = "nitrium_decomp"
+
+/datum/gas_reaction/nitrium_decomposition/init_reqs()
+ min_requirements = list(
+ GAS_NITRIUM = MINIMUM_MOLE_COUNT,
+ GAS_O2 = MINIMUM_MOLE_COUNT, // decomposes when in contact with Oxygen
+ "TEMP" = 1
+ )
+
+/datum/gas_reaction/nitrium_decomposition/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ if(temperature > NITRIUM_DECOMPOSITION_MAX_TEMP)
+ return NO_REACTION
+ var/nitrium_moles = air.get_moles(GAS_NITRIUM)
+ var/heat_efficiency = min(temperature / NITRIUM_DECOMPOSITION_TEMP_DIVISOR, nitrium_moles)
+ if(heat_efficiency <= 0)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ air.adjust_moles(GAS_NITRIUM, -heat_efficiency)
+ air.adjust_moles(GAS_N2, heat_efficiency)
+ air.adjust_moles(GAS_HYDROGEN, heat_efficiency)
+ var/energy_released = heat_efficiency * NITRIUM_DECOMPOSITION_ENERGY
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/pluox_formation
+ priority = 37
+ name = "Pluoxium Formation"
+ id = "pluox_formation"
+
+/datum/gas_reaction/pluox_formation/init_reqs()
+ min_requirements = list(
+ GAS_CO2 = MINIMUM_MOLE_COUNT,
+ GAS_O2 = MINIMUM_MOLE_COUNT,
+ GAS_TRITIUM = MINIMUM_MOLE_COUNT,
+ "TEMP" = PLUOXIUM_FORMATION_MIN_TEMP
+ )
+
+/datum/gas_reaction/pluox_formation/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ if(temperature > PLUOXIUM_FORMATION_MAX_TEMP)
+ return NO_REACTION
+ var/co2_moles = air.get_moles(GAS_CO2)
+ var/o2_moles = air.get_moles(GAS_O2)
+ var/tritium_moles = air.get_moles(GAS_TRITIUM)
+ // Consumption ratio 100 O2 : 50 CO2 : 1 Tritium per 50 pluoxium (i.e. 2 O2 : 1 CO2 : 0.01 Trit per 1 pluox)
+ var/produced_amount = min(PLUOXIUM_FORMATION_MAX_RATE, o2_moles * 0.5, co2_moles, tritium_moles * INVERSE(0.01))
+ if(produced_amount <= 0)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ air.adjust_moles(GAS_CO2, -produced_amount)
+ air.adjust_moles(GAS_O2, -produced_amount * 2)
+ air.adjust_moles(GAS_TRITIUM, -produced_amount * 0.01)
+ air.adjust_moles(GAS_PLUOXIUM, produced_amount)
+ air.adjust_moles(GAS_HYDROGEN, produced_amount * 0.01) // 1% H2 byproduct
+ var/energy_released = produced_amount * PLUOXIUM_FORMATION_ENERGY
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/proto_nitrate_formation
+ priority = 38
+ name = "Proto Nitrate Formation"
+ id = "proto_nitrate_formation"
+
+/datum/gas_reaction/proto_nitrate_formation/init_reqs()
+ min_requirements = list(
+ GAS_PLUOXIUM = MINIMUM_MOLE_COUNT,
+ GAS_HYDROGEN = MINIMUM_MOLE_COUNT,
+ "TEMP" = PN_FORMATION_MIN_TEMPERATURE
+ )
+
+/datum/gas_reaction/proto_nitrate_formation/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ if(temperature > PN_FORMATION_MAX_TEMPERATURE)
+ return NO_REACTION
+ var/pluox_moles = air.get_moles(GAS_PLUOXIUM)
+ var/h2_moles = air.get_moles(GAS_HYDROGEN)
+ var/heat_efficiency = min(temperature * 0.005, pluox_moles * INVERSE(0.2), h2_moles * INVERSE(2))
+ if(heat_efficiency <= 0)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ air.adjust_moles(GAS_HYDROGEN, -heat_efficiency * 2)
+ air.adjust_moles(GAS_PLUOXIUM, -heat_efficiency * 0.2)
+ air.adjust_moles(GAS_PROTO_NITRATE, heat_efficiency * 2.2)
+ var/energy_released = heat_efficiency * PN_FORMATION_ENERGY
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/proto_nitrate_hydrogen_response
+ priority = 25
+ name = "Proto Nitrate Hydrogen Response"
+ id = "proto_nitrate_hydrogen_response"
+
+/datum/gas_reaction/proto_nitrate_hydrogen_response/init_reqs()
+ min_requirements = list(
+ GAS_PROTO_NITRATE = MINIMUM_MOLE_COUNT,
+ GAS_HYDROGEN = PN_HYDROGEN_CONVERSION_THRESHOLD
+ )
+
+/datum/gas_reaction/proto_nitrate_hydrogen_response/react(datum/gas_mixture/air, datum/holder)
+ var/proto_moles = air.get_moles(GAS_PROTO_NITRATE)
+ var/h2_moles = air.get_moles(GAS_HYDROGEN)
+ var/produced_amount = min(PN_HYDROGEN_CONVERSION_MAX_RATE, h2_moles, proto_moles)
+ if(produced_amount <= 0)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ var/temperature = air.return_temperature()
+ air.adjust_moles(GAS_HYDROGEN, -produced_amount)
+ air.adjust_moles(GAS_PROTO_NITRATE, produced_amount * 0.5)
+ var/energy_used = produced_amount * PN_HYDROGEN_CONVERSION_ENERGY
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity - energy_used) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/proto_nitrate_tritium_response
+ priority = 26
+ name = "Proto Nitrate Tritium Response"
+ id = "proto_nitrate_tritium_response"
+
+/datum/gas_reaction/proto_nitrate_tritium_response/init_reqs()
+ min_requirements = list(
+ GAS_PROTO_NITRATE = MINIMUM_MOLE_COUNT,
+ GAS_TRITIUM = MINIMUM_MOLE_COUNT,
+ "TEMP" = PN_TRITIUM_CONVERSION_MIN_TEMP
+ )
+
+/datum/gas_reaction/proto_nitrate_tritium_response/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ if(temperature > PN_TRITIUM_CONVERSION_MAX_TEMP)
+ return NO_REACTION
+ var/proto_moles = air.get_moles(GAS_PROTO_NITRATE)
+ var/tritium_moles = air.get_moles(GAS_TRITIUM)
+ var/produced_amount = min(temperature / 34 * (tritium_moles * proto_moles) / (tritium_moles + 10 * proto_moles), tritium_moles, proto_moles * INVERSE(0.01))
+ if(produced_amount <= 0)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ air.adjust_moles(GAS_PROTO_NITRATE, -produced_amount * 0.01)
+ air.adjust_moles(GAS_TRITIUM, -produced_amount)
+ air.adjust_moles(GAS_HYDROGEN, produced_amount)
+ var/energy_released = produced_amount * PN_TRITIUM_CONVERSION_ENERGY
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/proto_nitrate_bz_response
+ priority = 27
+ name = "Proto Nitrate BZ Response"
+ id = "proto_nitrate_bz_response"
+
+/datum/gas_reaction/proto_nitrate_bz_response/init_reqs()
+ min_requirements = list(
+ GAS_PROTO_NITRATE = MINIMUM_MOLE_COUNT,
+ GAS_BZ = MINIMUM_MOLE_COUNT,
+ "TEMP" = PN_BZASE_MIN_TEMP
+ )
+
+/datum/gas_reaction/proto_nitrate_bz_response/react(datum/gas_mixture/air, datum/holder)
+ var/temperature = air.return_temperature()
+ if(temperature > PN_BZASE_MAX_TEMP)
+ return NO_REACTION
+ var/old_heat_capacity = air.heat_capacity()
+ var/proto_moles = air.get_moles(GAS_PROTO_NITRATE)
+ var/bz_moles = air.get_moles(GAS_BZ)
+ var/consumed_amount = min(temperature / 2240 * bz_moles * proto_moles / (bz_moles + proto_moles), bz_moles, proto_moles)
+ if(consumed_amount <= 0)
+ return NO_REACTION
+ air.adjust_moles(GAS_BZ, -consumed_amount)
+ air.adjust_moles(GAS_PROTO_NITRATE, -consumed_amount)
+ air.adjust_moles(GAS_N2, consumed_amount * 0.4)
+ air.adjust_moles(GAS_HELIUM, consumed_amount * 1.6)
+ air.adjust_moles(GAS_PLASMA, consumed_amount * 0.8)
+ var/energy_released = consumed_amount * PN_BZASE_ENERGY
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB))
+ return REACTING
+
+/datum/gas_reaction/antinoblium_replication
+ priority = 40
+ name = "Antinoblium Replication"
+ id = "antinoblium_replication"
+
+/datum/gas_reaction/antinoblium_replication/init_reqs()
+ min_requirements = list(
+ GAS_ANTINOBLIUM = MOLES_GAS_VISIBLE,
+ "TEMP" = REACTION_OPPRESSION_MIN_TEMP
+ )
+
+/datum/gas_reaction/antinoblium_replication/react(datum/gas_mixture/air, datum/holder)
+ var/old_heat_capacity = air.heat_capacity()
+ var/total_moles = air.total_moles()
+ var/antinoblium_moles = air.get_moles(GAS_ANTINOBLIUM)
+ var/total_not_antinoblium_moles = total_moles - antinoblium_moles
+ if(total_not_antinoblium_moles < MINIMUM_MOLE_COUNT)
+ return NO_REACTION
+ var/reaction_rate = min(antinoblium_moles / ANTINOBLIUM_CONVERSION_DIVISOR, total_not_antinoblium_moles)
+ var/list/gases = air.get_gases()
+ for(var/g in gases)
+ if(g == GAS_ANTINOBLIUM)
+ continue
+ var/m = air.get_moles(g)
+ if(m > 0)
+ air.adjust_moles(g, -reaction_rate * (m / total_not_antinoblium_moles))
+ air.adjust_moles(GAS_ANTINOBLIUM, reaction_rate)
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.set_temperature(max(air.return_temperature() * old_heat_capacity / new_heat_capacity, TCMB))
+ return REACTING
diff --git a/code/modules/atmospherics/machinery/components/electrolyzer/electrolyzer.dm b/code/modules/atmospherics/machinery/components/electrolyzer/electrolyzer.dm
new file mode 100644
index 0000000000000..5c7139de5d0f5
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/electrolyzer/electrolyzer.dm
@@ -0,0 +1,213 @@
+#define ELECTROLYZER_MODE_STANDBY "standby"
+#define ELECTROLYZER_MODE_WORKING "working"
+
+/obj/machinery/electrolyzer
+ anchored = FALSE
+ density = TRUE
+ interaction_flags_machine = INTERACT_MACHINE_ALLOW_SILICON | INTERACT_MACHINE_OPEN
+ icon = 'icons/obj/pipes_n_cables/atmos.dmi'
+ icon_state = "electrolyzer-off"
+ name = "space electrolyzer"
+ desc = "Thanks to the fast and dynamic response of our electrolyzers, on-site hydrogen production is guaranteed. Warranty void if used by clowns."
+ max_integrity = 250
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 100, BOMB = 0, BIO = 0, RAD = 0, FIRE = 80, ACID = 10)
+ circuit = /obj/item/circuitboard/machine/electrolyzer
+ use_power = NO_POWER_USE
+ var/obj/item/stock_parts/cell/cell
+ var/on = FALSE
+ var/mode = ELECTROLYZER_MODE_STANDBY
+ var/working_power = 1
+ var/efficiency = 0.5
+
+/obj/machinery/electrolyzer/get_cell()
+ return cell
+
+/obj/machinery/electrolyzer/Initialize(mapload)
+ . = ..()
+ SSair.start_processing_machine(src)
+ update_appearance(UPDATE_ICON)
+ register_context()
+
+/obj/machinery/electrolyzer/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Turn [on ? "off" : "on"]"
+ if(!held_item)
+ return CONTEXTUAL_SCREENTIP_SET
+ switch(held_item.tool_behaviour)
+ if(TOOL_SCREWDRIVER)
+ context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel"
+ if(TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unan" : "An"]chor"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/machinery/electrolyzer/Destroy()
+ if(cell)
+ QDEL_NULL(cell)
+ return ..()
+
+/obj/machinery/electrolyzer/on_deconstruction(disassembled)
+ if(cell)
+ LAZYADD(component_parts, cell)
+ cell = null
+ return ..()
+
+/obj/machinery/electrolyzer/examine(mob/user)
+ . = ..()
+ . += "\The [src] is [on ? "on" : "off"], and the panel is [panel_open ? "open" : "closed"]."
+ if(cell)
+ . += "The charge meter reads [cell ? round(cell.percent(), 1) : 0]%."
+ else
+ . += "There is no power cell installed."
+ if(in_range(user, src) || isobserver(user))
+ . += span_notice("Alt-click to toggle [on ? "off" : "on"].")
+ . += span_notice("It will drain power from the [anchored ? "area's APC" : "internal power cell"].")
+
+/obj/machinery/electrolyzer/update_icon_state()
+ icon_state = "electrolyzer-[on ? "[mode]" : "off"]"
+ return ..()
+
+/obj/machinery/electrolyzer/update_overlays()
+ . = ..()
+ if(panel_open)
+ . += "electrolyzer-open"
+
+/obj/machinery/electrolyzer/process_atmos()
+ if(!is_operational && on)
+ on = FALSE
+ if(!on)
+ return PROCESS_KILL
+ if((!cell || cell.charge <= 0) && !anchored)
+ on = FALSE
+ update_appearance(UPDATE_ICON)
+ return PROCESS_KILL
+
+ var/turf/our_turf = loc
+ if(!isturf(our_turf))
+ if(mode != ELECTROLYZER_MODE_STANDBY)
+ mode = ELECTROLYZER_MODE_STANDBY
+ update_appearance(UPDATE_ICON)
+ return
+
+ var/new_mode = on ? ELECTROLYZER_MODE_WORKING : ELECTROLYZER_MODE_STANDBY
+ if(mode != new_mode)
+ mode = new_mode
+ update_appearance(UPDATE_ICON)
+ if(mode == ELECTROLYZER_MODE_STANDBY)
+ return
+
+ var/datum/gas_mixture/env = our_turf.return_air()
+ if(!env)
+ return
+ call_reactions(env)
+ our_turf.air_update_turf(FALSE, FALSE)
+
+ var/power_to_use = (5 * (3 * working_power) * working_power) / (efficiency + working_power)
+ if(anchored)
+ use_power(power_to_use)
+ else
+ cell.use(power_to_use)
+
+/obj/machinery/electrolyzer/proc/call_reactions(datum/gas_mixture/env)
+ var/list/electrolyzer_args = list()
+ for(var/reaction_id in GLOB.electrolyzer_reactions)
+ var/datum/electrolyzer_reaction/R = GLOB.electrolyzer_reactions[reaction_id]
+ if(!R.reaction_check(env, electrolyzer_args))
+ continue
+ R.react(env, working_power, electrolyzer_args)
+
+/obj/machinery/electrolyzer/RefreshParts()
+ . = ..()
+ var/power = 0
+ var/cap = 0
+ for(var/obj/item/stock_parts/manipulator/M in component_parts)
+ power += M.rating
+ for(var/obj/item/stock_parts/capacitor/C in component_parts)
+ cap += C.rating
+ working_power = max(power, 1)
+ efficiency = (cap + 1) * 0.5
+
+/obj/machinery/electrolyzer/screwdriver_act(mob/living/user, obj/item/tool)
+ tool.play_tool_sound(src, 50)
+ panel_open = !panel_open
+ balloon_alert(user, "[panel_open ? "opened" : "closed"] panel")
+ update_appearance(UPDATE_ICON)
+ return TRUE
+
+/obj/machinery/electrolyzer/wrench_act(mob/living/user, obj/item/tool)
+ . = ..()
+ default_unfasten_wrench(user, tool)
+ return TRUE
+
+/obj/machinery/electrolyzer/crowbar_act(mob/living/user, obj/item/tool)
+ return default_deconstruction_crowbar(tool)
+
+/obj/machinery/electrolyzer/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers)
+ add_fingerprint(user)
+ if(istype(I, /obj/item/stock_parts/cell))
+ if(!panel_open)
+ balloon_alert(user, "open panel!")
+ return
+ if(cell)
+ balloon_alert(user, "cell inside!")
+ return
+ if(!user.transferItemToLoc(I, src))
+ return
+ cell = I
+ I.add_fingerprint(user)
+ balloon_alert(user, "inserted cell")
+ SStgui.update_uis(src)
+ return
+ return ..()
+
+/obj/machinery/electrolyzer/AltClick(mob/user)
+ if(panel_open)
+ balloon_alert(user, "close panel!")
+ return
+ toggle_power(user)
+
+/obj/machinery/electrolyzer/proc/toggle_power(mob/user)
+ if(!anchored && !cell)
+ balloon_alert(user, "insert cell or anchor!")
+ return
+ on = !on
+ mode = ELECTROLYZER_MODE_STANDBY
+ update_appearance(UPDATE_ICON)
+ balloon_alert(user, "turned [on ? "on" : "off"]")
+ if(on)
+ SSair.start_processing_machine(src)
+
+/obj/machinery/electrolyzer/ui_state(mob/user)
+ return GLOB.physical_state
+
+/obj/machinery/electrolyzer/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "Electrolyzer", name)
+ ui.open()
+
+/obj/machinery/electrolyzer/ui_data()
+ var/list/data = list()
+ data["open"] = panel_open
+ data["on"] = on
+ data["hasPowercell"] = !isnull(cell)
+ data["anchored"] = anchored
+ if(cell)
+ data["powerLevel"] = round(cell.percent(), 1)
+ return data
+
+/obj/machinery/electrolyzer/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("power")
+ toggle_power(ui.user)
+ . = TRUE
+ if("eject")
+ if(panel_open && cell)
+ cell.forceMove(drop_location())
+ cell = null
+ . = TRUE
+
+#undef ELECTROLYZER_MODE_STANDBY
+#undef ELECTROLYZER_MODE_WORKING
diff --git a/code/modules/atmospherics/machinery/components/electrolyzer/electrolyzer_reactions.dm b/code/modules/atmospherics/machinery/components/electrolyzer/electrolyzer_reactions.dm
new file mode 100644
index 0000000000000..daac2910bca39
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/electrolyzer/electrolyzer_reactions.dm
@@ -0,0 +1,75 @@
+// Electrolyzer reactions (from WhiteMoon). Uses string gas IDs (GAS_*).
+#define HALON_FORMATION_ENERGY 91232.1
+
+GLOBAL_LIST_INIT(electrolyzer_reactions, electrolyzer_reactions_list())
+
+/proc/electrolyzer_reactions_list()
+ var/list/built = list()
+ for(var/reaction_path in subtypesof(/datum/electrolyzer_reaction))
+ var/datum/electrolyzer_reaction/R = new reaction_path()
+ built[R.id] = R
+ return built
+
+/datum/electrolyzer_reaction
+ var/list/requirements
+ var/name = "reaction"
+ var/id = "r"
+ var/desc = ""
+ var/list/factor
+
+/datum/electrolyzer_reaction/proc/react(datum/gas_mixture/air_mixture, working_power, list/electrolyzer_args = list())
+ return
+
+/datum/electrolyzer_reaction/proc/reaction_check(datum/gas_mixture/air_mixture, list/electrolyzer_args = list())
+ var/temp = air_mixture.return_temperature()
+ if(requirements["MIN_TEMP"] && temp < requirements["MIN_TEMP"])
+ return FALSE
+ if(requirements["MAX_TEMP"] && temp > requirements["MAX_TEMP"])
+ return FALSE
+ for(var/gas_id in requirements)
+ if(gas_id == "MIN_TEMP" || gas_id == "MAX_TEMP")
+ continue
+ if(air_mixture.get_moles(gas_id) < requirements[gas_id])
+ return FALSE
+ return TRUE
+
+// H2O -> O2 + 2 H2
+/datum/electrolyzer_reaction/h2o_conversion
+ name = "H2O Conversion"
+ id = "h2o_conversion"
+ desc = "Conversion of H2O into O2 and H2"
+ requirements = list(GAS_H2O = MINIMUM_MOLE_COUNT)
+ factor = list()
+
+/datum/electrolyzer_reaction/h2o_conversion/react(datum/gas_mixture/air_mixture, working_power, list/electrolyzer_args = list())
+ var/old_heat = air_mixture.heat_capacity()
+ var/h2o_moles = air_mixture.get_moles(GAS_H2O)
+ var/proportion = min(h2o_moles * INVERSE(2), (2.5 * (working_power ** 2)))
+ air_mixture.adjust_moles(GAS_H2O, -proportion * 2)
+ air_mixture.adjust_moles(GAS_O2, proportion)
+ air_mixture.adjust_moles(GAS_HYDROGEN, proportion * 2)
+ var/new_heat = air_mixture.heat_capacity()
+ if(new_heat > MINIMUM_HEAT_CAPACITY)
+ air_mixture.set_temperature(max(air_mixture.return_temperature() * old_heat / new_heat, TCMB))
+
+// BZ -> O2 + Halon (temperature‑dependent efficiency)
+/datum/electrolyzer_reaction/halon_generation
+ name = "Halon generation"
+ id = "halon_generation"
+ desc = "Production of halon from the electrolysis of BZ."
+ requirements = list(GAS_BZ = MINIMUM_MOLE_COUNT)
+ factor = list()
+
+/datum/electrolyzer_reaction/halon_generation/react(datum/gas_mixture/air_mixture, working_power, list/electrolyzer_args = list())
+ var/old_heat = air_mixture.heat_capacity()
+ var/bz_moles = air_mixture.get_moles(GAS_BZ)
+ var/reaction_efficency = min(bz_moles * (1 - NUM_E ** (-0.5 * air_mixture.return_temperature() * working_power / FIRE_MINIMUM_TEMPERATURE_TO_EXIST)), bz_moles)
+ air_mixture.adjust_moles(GAS_BZ, -reaction_efficency)
+ air_mixture.adjust_moles(GAS_O2, reaction_efficency * 0.2)
+ air_mixture.adjust_moles(GAS_HALON, reaction_efficency * 2)
+ var/energy_used = reaction_efficency * HALON_FORMATION_ENERGY
+ var/new_heat = air_mixture.heat_capacity()
+ if(new_heat > MINIMUM_HEAT_CAPACITY)
+ air_mixture.set_temperature(max(((air_mixture.return_temperature() * old_heat + energy_used) / new_heat), TCMB))
+
+#undef HALON_FORMATION_ENERGY
diff --git a/code/modules/atmospherics/machinery/components/fusion/_hfr_defines.dm b/code/modules/atmospherics/machinery/components/fusion/_hfr_defines.dm
new file mode 100644
index 0000000000000..8fb88485f6e09
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/fusion/_hfr_defines.dm
@@ -0,0 +1,237 @@
+//
+// HFR formula constants (what they scale: modifiers, heat, damage, thresholds)
+//
+
+///Speed of light, in m/s
+#define LIGHT_SPEED 299792458
+/// Scale factor for (LIGHT_SPEED**2) to avoid 32-bit float overflow: compute (c²/scale) then multiply by scale.
+#define LIGHT_SPEED_SQ_SCALE 1e10
+#define LIGHT_SPEED_SQ_SCALED (LIGHT_SPEED * LIGHT_SPEED / LIGHT_SPEED_SQ_SCALE)
+///Calculation between the plank constant and the lambda of the lightwave
+#define PLANCK_LIGHT_CONSTANT 2e-16
+///Radius of the h2 calculated based on the amount of number of atom in a mole (and some addition for balancing issues)
+#define CALCULATED_H2RADIUS 120e-4
+///Radius of the trit calculated based on the amount of number of atom in a mole (and some addition for balancing issues)
+#define CALCULATED_TRITRADIUS 230e-3
+///Power conduction in the void, used to calculate the efficiency of the reaction
+#define VOID_CONDUCTION 1e-2
+/// Volume scaling for scaled_fuel/scaled_moderator lists (scale_factor = volume * this)
+#define HFR_VOLUME_SCALE 0.5
+/// Moderator gas counts toward gas_power at this fraction (fusion mix = 1.0)
+#define HFR_MODERATOR_GAS_POWER_FRAC 0.75
+/// Instability: current_damper contribution per 0.01
+#define HFR_INSTABILITY_DAMPER_FACTOR 0.01
+/// Instability: iron_content penalty per point
+#define HFR_INSTABILITY_IRON_PENALTY 0.05
+/// Modifier clamp min (power/heat), max 100
+#define HFR_MODIFIER_CLAMP_MIN 0.25
+#define HFR_MODIFIER_CLAMP_MAX 100
+/// Radiation modifier clamp min
+#define HFR_RADIATION_MODIFIER_MIN 0.005
+#define HFR_RADIATION_MODIFIER_MAX 1000
+/// Energy upper bound (float safety)
+#define HFR_ENERGY_CLAMP_MAX 1e35
+/// core_temperature from internal_power: divide by this
+#define HFR_CORE_TEMP_DIVISOR 1000
+/// Conduction: magnetic_constrictor multiplier (0.001 = 0.1% per point)
+#define HFR_CONDUCTION_MAGNETIC_FACTOR 0.001
+/// Radiation formula: Planck constant divisor (scale to usable range)
+#define HFR_PLANCK_RADIATION_DIVISOR 5e-18
+/// Heat limiter base: 5 * (10 ** power_level) * (heating_conductor/100)
+#define HFR_HEAT_LIMITER_BASE 5
+/// Heat output formula divisor (internal_instability * power_output * heat_modifier / this)
+#define HFR_HEAT_OUTPUT_DIVISOR 200
+/// Fuel consumption: fuel_injection_rate * this * power_level, then clamp
+#define HFR_FUEL_CONSUMPTION_RATE_FACTOR (0.01 * 5)
+#define HFR_FUEL_CONSUMPTION_CLAMP_MIN 0.05
+#define HFR_FUEL_CONSUMPTION_CLAMP_MAX 30
+/// Production at level 3/4: heat_output / this
+#define HFR_PRODUCTION_HEAT_DIVISOR 1000
+/// Production at other levels: heat_output * 2 / (10 ** (power_level+1))
+#define HFR_PRODUCTION_HEAT_MULT 2
+/// Fuel consumption in moderator_fuel_process: consumption_amount * this * fuel_consumption_multiplier
+#define HFR_FUEL_CONSUMPTION_MULT 0.85
+/// Primary product moles added per fuel_consumption
+#define HFR_PRIMARY_PRODUCTION_FRAC 0.5
+/// Heat limiter cooling: per-tick multiplier (heat_limiter_modifier * this * seconds_per_tick)
+#define HFR_COOLING_PER_TICK_FACTOR 0.01
+/// Evaporate moderator: (1 - (1 - this * power_level)^seconds_per_tick) fraction removed per tick
+#define HFR_EVAPORATE_RATE_BASE 0.0005
+/// Iron content damage: (round(iron_content)-1) * this * seconds_per_tick per point above 1
+#define HFR_IRON_DAMAGE_PER_POINT 2.5
+/// Healium heal: only when critical_threshold_proximity > this
+#define HFR_HEALIUM_HEAL_PROXIMITY_THRESHOLD 400
+/// Healium heal rate: (moderator_list[GAS_HEALIUM]/100) * this * melting_point * seconds_per_tick
+#define HFR_HEALIUM_HEAL_RATE_FACTOR 0.0011
+/// Antinoblium production temp threshold (K): below this or (plasma+BZ) condition
+#define HFR_ANTINOBLIUM_TEMP_THRESHOLD 1e7
+/// Overmole: moles above this trigger 2% integrity damage every 5 sec
+#define HFR_OVERMOLE_MOLES 5000
+/// Overmole: deciseconds between damage applications (5 sec)
+#define HFR_OVERMOLE_INTERVAL_DS 50
+/// Overmole: integrity damage per trigger (2% of melting_point)
+#define HFR_OVERMOLE_DAMAGE_FRAC 0.02
+/// Iron heal chance: 25 / (power_level+1) prob per tick when power_level <= 4
+#define HFR_IRON_HEAL_CHANCE_DIVISOR 25
+/// Iron passive decay when power_level <= 4: this * seconds_per_tick per tick
+#define HFR_IRON_DECAY_RATE 0.01
+/// Iron content clamp max
+#define HFR_IRON_CONTENT_MAX 5
+/// O2 iron heal: moderator_list[GAS_O2] > this to allow iron heal
+#define HFR_IRON_HEAL_O2_THRESHOLD 150
+/// Lightning/tesla: radiation_pulse range
+#define HFR_LIGHTNING_RADIATION_RANGE 6
+/// Lightning: only when moderator_list[GAS_ANTINOBLIUM] > this and proximity <= 500
+#define HFR_LIGHTNING_ANTINOBLIUM_MIN 50
+#define HFR_LIGHTNING_PROXIMITY_MAX 500
+
+/// Fallback when no power / unanchored: default values written each tick
+#define HFR_FALLBACK_MAGNETIC_CONSTRICTOR 100
+#define HFR_FALLBACK_HEATING_CONDUCTOR 500
+#define HFR_FALLBACK_CURRENT_DAMPER 0
+#define HFR_FALLBACK_FUEL_INJECTION_RATE 200
+#define HFR_FALLBACK_MODERATOR_INJECTION_RATE 500
+/// Iron accumulation per second when no power
+#define HFR_FALLBACK_IRON_RATE 0.10
+/// Volume = internal_fusion.return_volume() * (magnetic_constrictor * this)
+#define HFR_MAGNETIC_VOLUME_FRAC 0.01
+
+///Mole count required (tritium/hydrogen) to start a fusion reaction in HFR (reactions.dm uses 250 for other fusion)
+#define HFR_FUSION_MOLE_THRESHOLD 25
+///Used to reduce the gas_power to a more useful amount
+#ifndef INSTABILITY_GAS_POWER_FACTOR
+#define INSTABILITY_GAS_POWER_FACTOR 0.003
+#endif
+///Used to calculate the toroidal_size for the instability
+#ifndef TOROID_VOLUME_BREAKEVEN
+#define TOROID_VOLUME_BREAKEVEN 1000
+#endif
+///Constant used when calculating the chance of emitting a radioactive particle
+#ifndef PARTICLE_CHANCE_CONSTANT
+#define PARTICLE_CHANCE_CONSTANT (-20000000)
+#endif
+///Conduction of heat inside the fusion reactor
+#define METALLIC_VOID_CONDUCTIVITY 0.38
+///Conduction of heat near the external cooling loop (output gases at 95% of moderator temp)
+#define HIGH_EFFICIENCY_CONDUCTIVITY 0.95
+///Sets the minimum amount of power the machine uses
+#define MIN_POWER_USAGE (50 KILO WATTS)
+///Sets the multiplier for the damage
+#define DAMAGE_CAP_MULTIPLIER 0.005
+/// Max overmole (5000+ moles) damage per 5-second trigger so huge melting_point doesn't overshoot (still subject to cap)
+#define HYPERTORUS_OVERMOLE_MAX_ADD 50
+///Sets the range of the hallucinations
+#define HALLUCINATION_HFR(P) (min(7, round(abs(P) ** 0.25)))
+///Chance in percentage points per fusion level of iron accumulation when operating at unsafe levels
+#define IRON_CHANCE_PER_FUSION_LEVEL 17
+///Amount of iron accumulated per second whenever we fail our saving throw, using the chance above
+#define IRON_ACCUMULATED_PER_SECOND 0.005
+///Maximum amount of iron that can be healed per second. Calculated to mostly keep up with fusion level 5.
+#define IRON_OXYGEN_HEAL_PER_SECOND (IRON_ACCUMULATED_PER_SECOND * (100 - IRON_CHANCE_PER_FUSION_LEVEL) / 100)
+///Amount of oxygen in moles required to fully remove 100% iron content. Currently about 2409mol. Calculated to consume at most 10mol/s.
+#define OXYGEN_MOLES_CONSUMED_PER_IRON_HEAL (10 / IRON_OXYGEN_HEAL_PER_SECOND)
+
+//If integrity percent remaining is less than these values, the monitor sets off the relevant alarm.
+#define HYPERTORUS_MELTING_PERCENT 5
+#define HYPERTORUS_EMERGENCY_PERCENT 25
+#define HYPERTORUS_DANGER_PERCENT 50
+#define HYPERTORUS_WARNING_PERCENT 100
+
+#define WARNING_TIME_DELAY 60
+///to prevent accent sounds from layering
+#define HYPERTORUS_ACCENT_SOUND_MIN_COOLDOWN (3 SECONDS)
+
+#define HYPERTORUS_COUNTDOWN_TIME (30 SECONDS)
+
+//
+// Damage source: Too much mass in the fusion mix at high fusion levels
+//
+
+#define HYPERTORUS_OVERFULL_MIN_POWER_LEVEL 5
+#define HYPERTORUS_OVERFULL_MAX_SAFE_COLD_FUSION_MOLES 2700
+#define HYPERTORUS_OVERFULL_MAX_SAFE_HOT_FUSION_MOLES 1800
+#define HYPERTORUS_OVERFULL_MOLAR_SLOPE (1/80)
+#define HYPERTORUS_OVERFULL_TEMPERATURE_SLOPE (HYPERTORUS_OVERFULL_MOLAR_SLOPE * (HYPERTORUS_OVERFULL_MAX_SAFE_COLD_FUSION_MOLES - HYPERTORUS_OVERFULL_MAX_SAFE_HOT_FUSION_MOLES) / (FUSION_MAXIMUM_TEMPERATURE - 1))
+#define HYPERTORUS_OVERFULL_CONSTANT (-(HYPERTORUS_OVERFULL_MOLAR_SLOPE * HYPERTORUS_OVERFULL_MAX_SAFE_HOT_FUSION_MOLES + HYPERTORUS_OVERFULL_TEMPERATURE_SLOPE * FUSION_MAXIMUM_TEMPERATURE))
+
+//
+// Heal source: Small enough mass in the fusion mix
+//
+
+#define HYPERTORUS_SUBCRITICAL_MOLES 800
+#define HYPERTORUS_SUBCRITICAL_SCALE 150
+
+//
+// Heal source: Cold enough coolant
+//
+
+#define HYPERTORUS_COLD_COOLANT_MAX_RESTORE 2.5
+#define HYPERTORUS_COLD_COOLANT_THRESHOLD (10 ** 5)
+#define HYPERTORUS_COLD_COOLANT_SCALE (HYPERTORUS_COLD_COOLANT_MAX_RESTORE / log(10, HYPERTORUS_COLD_COOLANT_THRESHOLD))
+
+//
+// Damage source: Iron content
+//
+
+#define HYPERTORUS_MAX_SAFE_IRON 0.35
+
+//
+// Damage source: Extreme levels of mass in fusion mix at any power level
+//
+
+#define HYPERTORUS_HYPERCRITICAL_MOLES 10000
+#define HYPERTORUS_HYPERCRITICAL_SCALE 0.005
+#define HYPERTORUS_HYPERCRITICAL_MAX_DAMAGE 40
+
+#define HYPERTORUS_WEAK_SPILL_RATE 0.0005
+#define HYPERTORUS_WEAK_SPILL_CHANCE 1
+#define HYPERTORUS_MEDIUM_SPILL_PRESSURE 10000
+#define HYPERTORUS_MEDIUM_SPILL_INITIAL 0.25
+#define HYPERTORUS_MEDIUM_SPILL_RATE 0.01
+#define HYPERTORUS_STRONG_SPILL_PRESSURE 12000
+#define HYPERTORUS_STRONG_SPILL_INITIAL 0.75
+#define HYPERTORUS_STRONG_SPILL_RATE 0.05
+
+//
+// Explosion flags for use in fuel recipes
+//
+#define HYPERTORUS_FLAG_BASE_EXPLOSION (1<<0)
+#define HYPERTORUS_FLAG_MEDIUM_EXPLOSION (1<<1)
+#define HYPERTORUS_FLAG_DEVASTATING_EXPLOSION (1<<2)
+#define HYPERTORUS_FLAG_RADIATION_PULSE (1<<3)
+#define HYPERTORUS_FLAG_EMP (1<<4)
+#define HYPERTORUS_FLAG_MINIMUM_SPREAD (1<<5)
+#define HYPERTORUS_FLAG_MEDIUM_SPREAD (1<<6)
+#define HYPERTORUS_FLAG_BIG_SPREAD (1<<7)
+#define HYPERTORUS_FLAG_MASSIVE_SPREAD (1<<8)
+#define HYPERTORUS_FLAG_CRITICAL_MELTDOWN (1<<9)
+
+///High power damage
+#define HYPERTORUS_FLAG_HIGH_POWER_DAMAGE (1<<0)
+///High fuel mix mole
+#define HYPERTORUS_FLAG_HIGH_FUEL_MIX_MOLE (1<<1)
+///iron content damage
+#define HYPERTORUS_FLAG_IRON_CONTENT_DAMAGE (1<<2)
+///Iron content increasing
+#define HYPERTORUS_FLAG_IRON_CONTENT_INCREASE (1<<3)
+///Emped hypertorus
+#define HYPERTORUS_FLAG_EMPED (1<<4)
+
+// Status for get_status()
+#define HYPERTORUS_MELTING 1
+#define HYPERTORUS_EMERGENCY 2
+#define HYPERTORUS_DANGER 3
+#define HYPERTORUS_WARNING 4
+#define HYPERTORUS_NOMINAL 5
+#define HYPERTORUS_INACTIVE 6
+
+// BlueMoon compatibility (do not define zap/COMSIG here; supermatter.dm and modular define them)
+#ifndef BASE_MACHINE_IDLE_CONSUMPTION
+#define BASE_MACHINE_IDLE_CONSUMPTION 50
+#endif
+#ifndef ZAP_SUPERMATTER_FLAGS
+#define ZAP_SUPERMATTER_FLAGS 0
+#endif
+#ifndef AREA_USAGE_ENVIRON
+#define AREA_USAGE_ENVIRON ENVIRON
+#endif
diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm
new file mode 100644
index 0000000000000..fde45a37702f2
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm
@@ -0,0 +1,151 @@
+/**
+ * HFR core - variables, Initialize(), Destroy()
+ * BlueMoon: uses gas ID strings and get_moles/set_moles
+ */
+/obj/machinery/atmospherics/components/unary/hypertorus/core
+ name = "HFR core"
+ desc = "This is the Hypertorus Fusion Reactor core, an advanced piece of technology to finely tune the reaction inside of the machine. It has I/O for cooling gases."
+ icon = 'icons/obj/machines/atmospherics/hypertorus.dmi'
+ icon_state = "core_off"
+ circuit = /obj/item/circuitboard/machine/HFR_core
+ use_power = IDLE_POWER_USE
+ idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION
+ icon_state_open = "core_open"
+ icon_state_off = "core_off"
+ icon_state_active = "core_active"
+
+ var/start_power = FALSE
+ var/start_cooling = FALSE
+ var/start_fuel = FALSE
+ var/start_moderator = FALSE
+
+ var/obj/machinery/hypertorus/interface/linked_interface
+ var/obj/machinery/atmospherics/components/unary/hypertorus/moderator_input/linked_moderator
+ var/obj/machinery/atmospherics/components/unary/hypertorus/fuel_input/linked_input
+ var/obj/machinery/atmospherics/components/unary/hypertorus/waste_output/linked_output
+ var/list/corners = list()
+ var/list/machine_parts = list()
+ var/datum/gas_mixture/internal_fusion
+ var/datum/gas_mixture/moderator_internal
+ var/list/moderator_scrubbing = list(GAS_HELIUM)
+ var/moderator_filtering_rate = 20
+ var/datum/hfr_fuel/selected_fuel
+
+ var/energy = 0
+ var/core_temperature = T20C
+ var/internal_power = 0
+ var/power_output = 0
+ var/instability = 0
+ var/delta_temperature = 0
+ var/conduction = 0
+ var/radiation = 0
+ var/efficiency = 0
+ var/heat_limiter_modifier = 0
+ var/heat_output_max = 0
+ var/heat_output_min = 0
+ var/heat_output = 0
+
+ var/waste_remove = FALSE
+ var/heating_conductor = 100
+ var/magnetic_constrictor = 100
+ var/current_damper = 0
+ var/power_level = 0
+ var/iron_content = 0
+ var/fuel_injection_rate = 25
+ var/moderator_injection_rate = 25
+
+ var/critical_threshold_proximity = 0
+ var/critical_threshold_proximity_archived = 0
+ var/safe_alert = "Main containment field returning to safe operating parameters."
+ var/warning_point = 50
+ var/warning_alert = "Danger! Magnetic containment field faltering!"
+ var/emergency_point = 700
+ var/emergency_alert = "HYPERTORUS MELTDOWN IMMINENT."
+ var/melting_point = 900
+ var/has_reached_emergency = FALSE
+ var/lastwarning = 0
+
+ var/obj/item/radio/radio
+ var/radio_key = /obj/item/encryptionkey/headset_eng
+ var/engineering_channel = "Engineering"
+ var/common_channel = null
+
+ var/datum/looping_sound/hypertorus/soundloop
+ var/last_accent_sound = 0
+
+ var/fusion_temperature_archived = 0
+ var/fusion_temperature = 0
+ var/moderator_temperature_archived = 0
+ var/moderator_temperature = 0
+ var/coolant_temperature_archived = 0
+ var/coolant_temperature = 0
+ var/output_temperature_archived = 0
+ var/output_temperature = 0
+ var/temperature_period = 1
+ var/final_countdown = FALSE
+
+ var/warning_damage_flags = NONE
+ /// Last world.time when overmole (5000+) integrity damage was applied
+ var/last_overmole_damage = 0
+
+ /// Cached lists reused every process_atmos to avoid allocations (fusion_process)
+ var/list/hfr_fuel_list = list()
+ var/list/hfr_scaled_fuel_list = list()
+ var/list/hfr_moderator_list = list()
+ var/list/hfr_scaled_moderator_list = list()
+ /// Reused gas_mixture for output each tick instead of new
+ var/datum/gas_mixture/hfr_internal_output
+ /// Reused for remove_specific in remove_waste and inject_fuel to avoid 10+ allocations per tick
+ var/datum/gas_mixture/hfr_removed_waste
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/Initialize(mapload)
+ . = ..()
+ internal_fusion = new(5000)
+ moderator_internal = new(10000)
+ hfr_internal_output = new
+ hfr_removed_waste = new
+
+ radio = new(src)
+ radio.keyslot = new radio_key
+ radio.recalculateChannels()
+ investigate_log("has been created.", INVESTIGATE_HYPERTORUS)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/Destroy()
+ unregister_signals(TRUE)
+ if(internal_fusion)
+ internal_fusion = null
+ if(moderator_internal)
+ moderator_internal = null
+ if(linked_input)
+ QDEL_NULL(linked_input)
+ if(linked_output)
+ QDEL_NULL(linked_output)
+ if(linked_moderator)
+ QDEL_NULL(linked_moderator)
+ if(linked_interface)
+ QDEL_NULL(linked_interface)
+ var/list/corners_to_del = corners.Copy()
+ for(var/obj/machinery/hypertorus/corner/corner in corners_to_del)
+ QDEL_NULL(corner)
+ QDEL_NULL(radio)
+ QDEL_NULL(soundloop)
+ machine_parts = null
+ return ..()
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/on_deconstruction(disassembled)
+ var/turf/local_turf = get_turf(loc)
+ var/datum/gas_mixture/to_release = moderator_internal || internal_fusion
+ if(to_release == moderator_internal && internal_fusion)
+ to_release.merge(internal_fusion)
+ if(to_release && to_release.total_moles() > 0)
+ local_turf.assume_air(to_release)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/crowbar_act(mob/living/user, obj/item/tool)
+ var/internal_pressure = 0
+ if(internal_fusion)
+ internal_pressure = max(internal_pressure, internal_fusion.return_pressure())
+ if(moderator_internal)
+ internal_pressure = max(internal_pressure, moderator_internal.return_pressure())
+ if(internal_pressure > 0)
+ say("WARNING - Core can contain hazardous gases, deconstruct with caution!")
+ return default_deconstruction_crowbar(tool)
diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_fuel_datums.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_fuel_datums.dm
new file mode 100644
index 0000000000000..b0bf25b843539
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/fusion/hfr_fuel_datums.dm
@@ -0,0 +1,130 @@
+///Global list of recipes for atmospheric machines to use (id -> datum)
+GLOBAL_LIST_INIT(hfr_fuels_list, hfr_fuels_create_list())
+
+/proc/hfr_fuels_create_list()
+ . = list()
+ for(var/fuel_mix_path in subtypesof(/datum/hfr_fuel))
+ var/datum/hfr_fuel/fuel_mix = new fuel_mix_path()
+ if(!fuel_mix.id || .[fuel_mix.id])
+ stack_trace("hfr_fuel: skipping [fuel_mix_path] with empty or duplicate id '[fuel_mix.id]'")
+ continue
+ .[fuel_mix.id] = fuel_mix
+
+/datum/hfr_fuel
+ var/id = ""
+ var/name = ""
+ var/negative_temperature_multiplier = 1
+ var/positive_temperature_multiplier = 1
+ var/energy_concentration_multiplier = 1
+ var/fuel_consumption_multiplier = 1
+ var/gas_production_multiplier = 1
+ var/temperature_change_multiplier = 1
+ /// Main fuels: list of gas ID strings (e.g. GAS_PLASMA, GAS_O2)
+ var/list/requirements = list()
+ /// Gases produced in internal fusion mix: list of gas ID strings
+ var/list/primary_products = list()
+ /// Gases produced in moderator (power level 1-6): list of 6 gas ID strings
+ var/list/secondary_products = list()
+ var/meltdown_flags = HYPERTORUS_FLAG_BASE_EXPLOSION
+
+/datum/hfr_fuel/New()
+ . = ..()
+ temperature_change_multiplier = min(temperature_change_multiplier, 1)
+
+/datum/hfr_fuel/plasma_oxy_fuel
+ id = "plasma_o2_fuel"
+ name = "Plasma + Oxygen fuel"
+ negative_temperature_multiplier = 2.5
+ positive_temperature_multiplier = 0.1
+ energy_concentration_multiplier = 10
+ fuel_consumption_multiplier = 3.3
+ gas_production_multiplier = 1.4
+ temperature_change_multiplier = 0.6
+ requirements = list(GAS_PLASMA, GAS_O2)
+ primary_products = list(GAS_CO2, GAS_H2O)
+ secondary_products = list(GAS_CO2, GAS_H2O, GAS_FREON, GAS_NITROUS, GAS_PLUOXIUM, GAS_HALON)
+ meltdown_flags = HYPERTORUS_FLAG_BASE_EXPLOSION | HYPERTORUS_FLAG_MINIMUM_SPREAD
+
+/datum/hfr_fuel/hydrogen_oxy_fuel
+ id = "h2_o2_fuel"
+ name = "Hydrogen + Oxygen fuel"
+ negative_temperature_multiplier = 2
+ positive_temperature_multiplier = 0.6
+ energy_concentration_multiplier = 3
+ fuel_consumption_multiplier = 1.1
+ gas_production_multiplier = 0.9
+ temperature_change_multiplier = 0.75
+ requirements = list(GAS_HYDROGEN, GAS_O2)
+ primary_products = list(GAS_HELIUM, GAS_N2)
+ secondary_products = list(GAS_HELIUM, GAS_PLASMA, GAS_O2, GAS_N2, GAS_BZ, GAS_HYPERNOB)
+ meltdown_flags = HYPERTORUS_FLAG_BASE_EXPLOSION | HYPERTORUS_FLAG_EMP | HYPERTORUS_FLAG_MEDIUM_SPREAD
+
+/datum/hfr_fuel/tritium_oxy_fuel
+ id = "t2_o2_fuel"
+ name = "Tritium + Oxygen fuel"
+ negative_temperature_multiplier = 2.1
+ positive_temperature_multiplier = 0.5
+ energy_concentration_multiplier = 2
+ fuel_consumption_multiplier = 1.2
+ gas_production_multiplier = 0.8
+ temperature_change_multiplier = 0.8
+ requirements = list(GAS_TRITIUM, GAS_O2)
+ primary_products = list(GAS_HELIUM, GAS_PLUOXIUM)
+ secondary_products = list(GAS_HELIUM, GAS_PLASMA, GAS_O2, GAS_N2, GAS_BZ, GAS_HYPERNOB)
+ meltdown_flags = HYPERTORUS_FLAG_BASE_EXPLOSION | HYPERTORUS_FLAG_RADIATION_PULSE | HYPERTORUS_FLAG_MEDIUM_SPREAD
+
+/datum/hfr_fuel/hydrogen_tritium_fuel
+ id = "h2_t2_fuel"
+ name = "Hydrogen + Tritium fuel"
+ negative_temperature_multiplier = 1
+ positive_temperature_multiplier = 1
+ energy_concentration_multiplier = 1
+ fuel_consumption_multiplier = 1
+ gas_production_multiplier = 1
+ temperature_change_multiplier = 0.85
+ requirements = list(GAS_HYDROGEN, GAS_TRITIUM)
+ primary_products = list(GAS_HELIUM)
+ secondary_products = list(GAS_HELIUM, GAS_PLASMA, GAS_O2, GAS_N2, GAS_BZ, GAS_HYPERNOB)
+ meltdown_flags = HYPERTORUS_FLAG_MEDIUM_EXPLOSION | HYPERTORUS_FLAG_RADIATION_PULSE | HYPERTORUS_FLAG_EMP | HYPERTORUS_FLAG_MEDIUM_SPREAD
+
+/datum/hfr_fuel/hypernob_hydrogen_fuel
+ id = "hypernob_hydrogen_fuel"
+ name = "Hypernoblium + Hydrogen fuel"
+ negative_temperature_multiplier = 0.2
+ positive_temperature_multiplier = 2.2
+ energy_concentration_multiplier = 0.2
+ fuel_consumption_multiplier = 0.55
+ gas_production_multiplier = 1.4
+ temperature_change_multiplier = 0.9
+ requirements = list(GAS_HYPERNOB, GAS_HYDROGEN)
+ primary_products = list(GAS_ANTINOBLIUM)
+ secondary_products = list(GAS_ANTINOBLIUM, GAS_HELIUM, GAS_PROTO_NITRATE, GAS_ZAUKER, GAS_HEALIUM, GAS_MIASMA)
+ meltdown_flags = HYPERTORUS_FLAG_DEVASTATING_EXPLOSION | HYPERTORUS_FLAG_RADIATION_PULSE | HYPERTORUS_FLAG_EMP | HYPERTORUS_FLAG_BIG_SPREAD
+
+/datum/hfr_fuel/hypernob_trit_fuel
+ id = "hypernob_trit_fuel"
+ name = "Hypernoblium + Tritium fuel"
+ negative_temperature_multiplier = 0.1
+ positive_temperature_multiplier = 2.5
+ energy_concentration_multiplier = 0.1
+ fuel_consumption_multiplier = 0.45
+ gas_production_multiplier = 1.7
+ temperature_change_multiplier = 0.95
+ requirements = list(GAS_HYPERNOB, GAS_TRITIUM)
+ primary_products = list(GAS_ANTINOBLIUM)
+ secondary_products = list(GAS_ANTINOBLIUM, GAS_HELIUM, GAS_PROTO_NITRATE, GAS_ZAUKER, GAS_HEALIUM, GAS_MIASMA)
+ meltdown_flags = HYPERTORUS_FLAG_DEVASTATING_EXPLOSION | HYPERTORUS_FLAG_RADIATION_PULSE | HYPERTORUS_FLAG_EMP | HYPERTORUS_FLAG_BIG_SPREAD
+
+/datum/hfr_fuel/hypernob_antinob_fuel
+ id = "hypernob_antinob_fuel"
+ name = "Hypernoblium + Antinoblium fuel"
+ negative_temperature_multiplier = 0.01
+ positive_temperature_multiplier = 3.5
+ energy_concentration_multiplier = 2
+ fuel_consumption_multiplier = 0.01
+ gas_production_multiplier = 3
+ temperature_change_multiplier = 1
+ requirements = list(GAS_HYPERNOB, GAS_ANTINOBLIUM)
+ primary_products = list(GAS_HELIUM)
+ secondary_products = list(GAS_PLASMA, GAS_O2, GAS_N2, GAS_PROTO_NITRATE, GAS_NITRIUM, GAS_MIASMA)
+ meltdown_flags = HYPERTORUS_FLAG_DEVASTATING_EXPLOSION | HYPERTORUS_FLAG_RADIATION_PULSE | HYPERTORUS_FLAG_EMP | HYPERTORUS_FLAG_MASSIVE_SPREAD | HYPERTORUS_FLAG_CRITICAL_MELTDOWN
diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm
new file mode 100644
index 0000000000000..aadf95ae6a932
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm
@@ -0,0 +1,540 @@
+// Stub: no hallucination pulse in BlueMoon
+/proc/visible_hallucination_pulse(atom/source, range, duration)
+ return
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/process_atmos(seconds_per_tick)
+ /*
+ *Pre-checks
+ */
+ if(!active)
+ return
+
+ if(!check_part_connectivity())
+ deactivate()
+ return
+
+ CHECK_TICK
+
+ if (start_power || power_level)
+ play_ambience(seconds_per_tick)
+ fusion_process(seconds_per_tick)
+ CHECK_TICK
+ process_moderator_overflow(seconds_per_tick)
+ CHECK_TICK
+ process_damageheal(seconds_per_tick)
+ CHECK_TICK
+ check_alert()
+ if (start_power)
+ remove_waste(seconds_per_tick)
+ CHECK_TICK
+ update_pipenets()
+
+ check_deconstructable()
+
+ if(linked_interface)
+ SStgui.update_uis(linked_interface)
+
+/// Считает мощность, нестабильность, тепло и газы за тик. Без питания выставляет фолбек-значения и копит железо.
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/fusion_process(seconds_per_tick)
+ CHECK_TICK
+ if (check_power_use())
+ if (start_cooling)
+ inject_from_side_components(seconds_per_tick)
+ process_internal_cooling(seconds_per_tick)
+ else
+ magnetic_constrictor = HFR_FALLBACK_MAGNETIC_CONSTRICTOR
+ heating_conductor = HFR_FALLBACK_HEATING_CONDUCTOR
+ current_damper = HFR_FALLBACK_CURRENT_DAMPER
+ fuel_injection_rate = HFR_FALLBACK_FUEL_INJECTION_RATE
+ moderator_injection_rate = HFR_FALLBACK_MODERATOR_INJECTION_RATE
+ waste_remove = FALSE
+ iron_content += HFR_FALLBACK_IRON_RATE * seconds_per_tick
+
+ update_temperature_status(seconds_per_tick)
+
+ // Объём учёта: от объёма смеси и магнитного сужения (проценты). Дальше от него scale_factor и торoidal_size.
+ var/archived_heat = internal_fusion.return_temperature()
+ var/volume = internal_fusion.return_volume() * (magnetic_constrictor * HFR_MAGNETIC_VOLUME_FRAC)
+
+ var/energy_concentration_multiplier = 1
+ var/positive_temperature_multiplier = 1
+ var/negative_temperature_multiplier = 1
+
+ var/scale_factor = volume * HFR_VOLUME_SCALE
+
+ hfr_fuel_list.Cut()
+ hfr_scaled_fuel_list.Cut()
+ if (selected_fuel)
+ energy_concentration_multiplier = selected_fuel.energy_concentration_multiplier
+ positive_temperature_multiplier = selected_fuel.positive_temperature_multiplier
+ negative_temperature_multiplier = selected_fuel.negative_temperature_multiplier
+
+ for(var/gas_id in selected_fuel.requirements | selected_fuel.primary_products)
+ var/amount = internal_fusion.get_moles(gas_id)
+ hfr_fuel_list[gas_id] = amount
+ hfr_scaled_fuel_list[gas_id] = max((amount - HFR_FUSION_MOLE_THRESHOLD) / scale_factor, 0)
+
+ hfr_moderator_list.Cut()
+ hfr_scaled_moderator_list.Cut()
+ var/list/moderator_gases = moderator_internal.get_gases()
+ for(var/gas_id in moderator_gases)
+ var/amount = moderator_internal.get_moles(gas_id)
+ hfr_moderator_list[gas_id] = amount
+ hfr_scaled_moderator_list[gas_id] = max((amount - HFR_FUSION_MOLE_THRESHOLD) / scale_factor, 0)
+
+ CHECK_TICK
+
+ // Нестабильность: gas_power по fusion_powers из обеих смесей, потом (gas_power*factor)^2 mod toroidal_size плюс дампер, минус железо.
+ var/toroidal_size = (2 * PI) + TORADIANS(arctan((volume - TOROID_VOLUME_BREAKEVEN) / TOROID_VOLUME_BREAKEVEN))
+ var/list/fusion_powers = GLOB.gas_data.fusion_powers
+ var/gas_power = 0
+ var/list/fusion_gases = internal_fusion.get_gases()
+ for (var/gas_id in fusion_gases)
+ gas_power += (fusion_powers[gas_id] * internal_fusion.get_moles(gas_id))
+ for (var/gas_id in moderator_gases)
+ gas_power += (fusion_powers[gas_id] * moderator_internal.get_moles(gas_id) * HFR_MODERATOR_GAS_POWER_FRAC)
+
+ instability = MODULUS((gas_power * INSTABILITY_GAS_POWER_FACTOR)**2, toroidal_size) + (current_damper * HFR_INSTABILITY_DAMPER_FACTOR) - iron_content * HFR_INSTABILITY_IRON_PENALTY
+ // Знак нестабильности: ниже порога эндотермичность (1), иначе экзотермичность (-1). Влияет на знак heat_output.
+ var/internal_instability = 0
+ if(instability * 0.5 < FUSION_INSTABILITY_ENDOTHERMALITY)
+ internal_instability = 1
+ else
+ internal_instability = -1
+
+ // Модификаторы от модератора и топлива: вклад каждого газа (scaled), потом clamp. Входят в energy, power_output, heat, radiation.
+ var/energy_modifiers = hfr_scaled_moderator_list[GAS_N2] * 0.35 + \
+ hfr_scaled_moderator_list[GAS_CO2] * 0.55 + \
+ hfr_scaled_moderator_list[GAS_NITROUS] * 0.95 + \
+ hfr_scaled_moderator_list[GAS_ZAUKER] * 1.55 + \
+ hfr_scaled_moderator_list[GAS_ANTINOBLIUM] * 20
+ energy_modifiers -= hfr_scaled_moderator_list[GAS_HYPERNOB] * 10 + \
+ hfr_scaled_moderator_list[GAS_H2O] * 0.75 + \
+ hfr_scaled_moderator_list[GAS_NITRIUM] * 0.15 + \
+ hfr_scaled_moderator_list[GAS_HEALIUM] * 0.45 + \
+ hfr_scaled_moderator_list[GAS_FREON] * 1.15
+ var/power_modifier = hfr_scaled_moderator_list[GAS_O2] * 0.55 + \
+ hfr_scaled_moderator_list[GAS_CO2] * 0.95 + \
+ hfr_scaled_moderator_list[GAS_NITRIUM] * 1.45 + \
+ hfr_scaled_moderator_list[GAS_ZAUKER] * 5.55 + \
+ hfr_scaled_moderator_list[GAS_PLASMA] * 0.05 - \
+ hfr_scaled_moderator_list[GAS_NITROUS] * 0.05 - \
+ hfr_scaled_moderator_list[GAS_FREON] * 0.75
+ var/heat_modifier = hfr_scaled_moderator_list[GAS_PLASMA] * 1.25 - \
+ hfr_scaled_moderator_list[GAS_N2] * 0.75 - \
+ hfr_scaled_moderator_list[GAS_NITROUS] * 1.45 - \
+ hfr_scaled_moderator_list[GAS_FREON] * 0.95
+ var/radiation_modifier = hfr_scaled_moderator_list[GAS_FREON] * 1.15 - \
+ hfr_scaled_moderator_list[GAS_N2] * 0.45 - \
+ hfr_scaled_moderator_list[GAS_PLASMA] * 0.95 + \
+ hfr_scaled_moderator_list[GAS_BZ] * 1.9 + \
+ hfr_scaled_moderator_list[GAS_PROTO_NITRATE] * 0.1 + \
+ hfr_scaled_moderator_list[GAS_ANTINOBLIUM] * 10
+
+ if (selected_fuel)
+ energy_modifiers += hfr_scaled_fuel_list[selected_fuel.requirements[1]] + \
+ hfr_scaled_fuel_list[selected_fuel.requirements[2]]
+ energy_modifiers -= hfr_scaled_fuel_list[selected_fuel.primary_products[1]]
+
+ power_modifier += hfr_scaled_fuel_list[selected_fuel.requirements[2]] * 1.05 - \
+ hfr_scaled_fuel_list[selected_fuel.primary_products[1]] * 0.55
+
+ heat_modifier += hfr_scaled_fuel_list[selected_fuel.requirements[1]] * 1.15 + \
+ hfr_scaled_fuel_list[selected_fuel.primary_products[1]] * 1.05
+
+ radiation_modifier += hfr_scaled_fuel_list[selected_fuel.primary_products[1]]
+
+ power_modifier = clamp(power_modifier, HFR_MODIFIER_CLAMP_MIN, HFR_MODIFIER_CLAMP_MAX)
+ heat_modifier = clamp(heat_modifier, HFR_MODIFIER_CLAMP_MIN, HFR_MODIFIER_CLAMP_MAX)
+ radiation_modifier = clamp(radiation_modifier, HFR_RADIATION_MODIFIER_MIN, HFR_RADIATION_MODIFIER_MAX)
+
+ internal_power = 0
+ efficiency = VOID_CONDUCTION * 1
+
+ // Внутренняя мощность: произведение по двум требованиям топлива (scaled*mod/100), площадь поперечника (радиусы H2/трит), и energy. Эффективность от первичного продукта.
+ if (selected_fuel)
+ internal_power = (hfr_scaled_fuel_list[selected_fuel.requirements[1]] * power_modifier / 100) * (hfr_scaled_fuel_list[selected_fuel.requirements[2]] * power_modifier / 100) * (PI * (2 * (hfr_scaled_fuel_list[selected_fuel.requirements[1]] * CALCULATED_H2RADIUS) * (hfr_scaled_fuel_list[selected_fuel.requirements[2]] * CALCULATED_TRITRADIUS))**2) * energy
+
+ efficiency = VOID_CONDUCTION * clamp(hfr_scaled_fuel_list[selected_fuel.primary_products[1]], 1, 100)
+
+ // Energy: модификаторы * c² * (темп * heat_mod/100), с масштабом чтобы не переполнить float. Дальше core_temperature, conduction, radiation, power_output.
+ energy = (energy_modifiers * LIGHT_SPEED_SQ_SCALED) * max(internal_fusion.return_temperature() * heat_modifier / 100, 1) * LIGHT_SPEED_SQ_SCALE
+ energy = energy / energy_concentration_multiplier
+ energy = clamp(energy, 0, HFR_ENERGY_CLAMP_MAX)
+ core_temperature = internal_power * power_modifier / HFR_CORE_TEMP_DIVISOR
+ core_temperature = max(TCMB, core_temperature)
+ delta_temperature = archived_heat - core_temperature
+ conduction = - delta_temperature * (magnetic_constrictor * HFR_CONDUCTION_MAGNETIC_FACTOR)
+ radiation = max(-(PLANCK_LIGHT_CONSTANT / HFR_PLANCK_RADIATION_DIVISOR) * radiation_modifier * delta_temperature, 0)
+ power_output = efficiency * (internal_power - conduction - radiation)
+ // Лимиты тепла: от уровня и heating_conductor. heat_output от нестабильности и power_output, ограничен min/max.
+ heat_limiter_modifier = HFR_HEAT_LIMITER_BASE * (10 ** power_level) * (heating_conductor * HFR_MAGNETIC_VOLUME_FRAC)
+ heat_output_min = - heat_limiter_modifier * HFR_COOLING_PER_TICK_FACTOR * negative_temperature_multiplier
+ heat_output_max = heat_limiter_modifier * positive_temperature_multiplier
+ heat_output = clamp(internal_instability * power_output * heat_modifier / HFR_HEAT_OUTPUT_DIVISOR, heat_output_min, heat_output_max)
+
+ if (!check_fuel())
+ return
+
+ // Расход и производство за тик: consumption от уровня и fuel_injection_rate; production от heat_output и уровня (на 3/4 уровне по одному правилу, на остальных по другому).
+ var/fuel_consumption_rate = clamp(fuel_injection_rate * HFR_FUEL_CONSUMPTION_RATE_FACTOR * power_level, HFR_FUEL_CONSUMPTION_CLAMP_MIN, HFR_FUEL_CONSUMPTION_CLAMP_MAX)
+ var/consumption_amount = fuel_consumption_rate * seconds_per_tick
+ var/production_amount
+ switch(power_level)
+ if(3,4)
+ production_amount = clamp(heat_output / HFR_PRODUCTION_HEAT_DIVISOR, 0, fuel_consumption_rate) * seconds_per_tick
+ else
+ production_amount = clamp(heat_output * HFR_PRODUCTION_HEAT_MULT / 10 ** (power_level+1), 0, fuel_consumption_rate) * seconds_per_tick
+
+ var/dirty_production_rate = hfr_scaled_fuel_list[selected_fuel.primary_products[1]] / fuel_injection_rate
+
+ hfr_internal_output.clear()
+ moderator_fuel_process(seconds_per_tick, production_amount, consumption_amount, hfr_internal_output, hfr_moderator_list, selected_fuel, hfr_fuel_list)
+
+ CHECK_TICK
+
+ var/common_production_amount = production_amount * selected_fuel.gas_production_multiplier
+ moderator_common_process(seconds_per_tick, common_production_amount, hfr_internal_output, hfr_moderator_list, dirty_production_rate, heat_output, radiation_modifier)
+
+/// Топливо: вычитаем из fusion по requirements, добавляем primary_products. В модератор по уровням (tier) добавляем вторичные продукты. Коэффициенты по уровням захардкожены.
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/moderator_fuel_process(seconds_per_tick, production_amount, consumption_amount, datum/gas_mixture/internal_output, moderator_list, datum/hfr_fuel/fuel, fuel_list)
+ var/fuel_consumption = consumption_amount * HFR_FUEL_CONSUMPTION_MULT * selected_fuel.fuel_consumption_multiplier
+ var/scaled_production = production_amount * selected_fuel.gas_production_multiplier
+
+ for(var/gas_id in fuel.requirements)
+ internal_fusion.adjust_moles(gas_id, -min(fuel_list[gas_id], fuel_consumption))
+ for(var/gas_id in fuel.primary_products)
+ internal_fusion.adjust_moles(gas_id, fuel_consumption * HFR_PRIMARY_PRODUCTION_FRAC)
+
+ var/list/tier = fuel.secondary_products
+ switch(power_level)
+ if(1)
+ moderator_internal.adjust_moles(tier[1], scaled_production * 0.95)
+ moderator_internal.adjust_moles(tier[2], scaled_production * 0.75)
+ if(2)
+ moderator_internal.adjust_moles(tier[1], scaled_production * 1.65)
+ moderator_internal.adjust_moles(tier[2], scaled_production)
+ if(moderator_list[GAS_PLASMA] > 50)
+ moderator_internal.adjust_moles(tier[3], scaled_production * 1.15)
+ if(3)
+ moderator_internal.adjust_moles(tier[2], scaled_production * 0.5)
+ moderator_internal.adjust_moles(tier[3], scaled_production * 0.45)
+ if(4)
+ moderator_internal.adjust_moles(tier[3], scaled_production * 1.65)
+ moderator_internal.adjust_moles(tier[4], scaled_production * 1.25)
+ if(5)
+ moderator_internal.adjust_moles(tier[4], scaled_production * 0.65)
+ moderator_internal.adjust_moles(tier[5], scaled_production)
+ moderator_internal.adjust_moles(tier[6], scaled_production * 0.75)
+ if(6)
+ moderator_internal.adjust_moles(tier[5], scaled_production * 0.35)
+ moderator_internal.adjust_moles(tier[6], scaled_production)
+
+/// Выход в output: по уровням 1–6 от количества модератора (BZ, plasma, proto_nitrate и т.д.) добавляем газы в internal_output, правим radiation/heat. Healium при proximity > порога уменьшает proximity и съедает GAS_HEALIUM.
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/moderator_common_process(seconds_per_tick, scaled_production, datum/gas_mixture/internal_output, moderator_list, dirty_production_rate, heat_output, radiation_modifier)
+ switch(power_level)
+ if(1)
+ if(moderator_list[GAS_PLASMA] > 100)
+ internal_output.adjust_moles(GAS_NITROUS, scaled_production * 0.5)
+ moderator_internal.adjust_moles(GAS_PLASMA, -min(moderator_internal.get_moles(GAS_PLASMA), scaled_production * 0.85))
+ if(moderator_list[GAS_BZ] > 150)
+ internal_output.adjust_moles(GAS_HALON, scaled_production * 0.55)
+ moderator_internal.adjust_moles(GAS_BZ, -min(moderator_internal.get_moles(GAS_BZ), scaled_production * 0.95))
+ if(2)
+ if(moderator_list[GAS_PLASMA] > 50)
+ internal_output.adjust_moles(GAS_BZ, scaled_production * 1.8)
+ moderator_internal.adjust_moles(GAS_PLASMA, -min(moderator_internal.get_moles(GAS_PLASMA), scaled_production * 1.75))
+ if(moderator_list[GAS_PROTO_NITRATE] > 20)
+ radiation *= 1.55
+ heat_output *= 1.025
+ internal_output.adjust_moles(GAS_NITRIUM, scaled_production * 1.05)
+ moderator_internal.adjust_moles(GAS_PROTO_NITRATE, -min(moderator_internal.get_moles(GAS_PROTO_NITRATE), scaled_production * 1.35))
+ if(3, 4)
+ if(moderator_list[GAS_PLASMA] > 10)
+ internal_output.adjust_moles(GAS_FREON, scaled_production * 0.15)
+ internal_output.adjust_moles(GAS_NITRIUM, scaled_production * 1.05)
+ moderator_internal.adjust_moles(GAS_PLASMA, -min(moderator_internal.get_moles(GAS_PLASMA), scaled_production * 0.45))
+ if(moderator_list[GAS_FREON] > 50)
+ heat_output *= 0.9
+ radiation *= 0.8
+ if(moderator_list[GAS_PROTO_NITRATE] > 15)
+ internal_output.adjust_moles(GAS_NITRIUM, scaled_production * 1.25)
+ internal_output.adjust_moles(GAS_HALON, scaled_production * 1.15)
+ moderator_internal.adjust_moles(GAS_PROTO_NITRATE, -min(moderator_internal.get_moles(GAS_PROTO_NITRATE), scaled_production * 1.55))
+ radiation *= 1.95
+ heat_output *= 1.25
+ if(moderator_list[GAS_BZ] > 100)
+ internal_output.adjust_moles(GAS_HEALIUM, scaled_production * 1.5)
+ internal_output.adjust_moles(GAS_PROTO_NITRATE, scaled_production * 1.5)
+ visible_hallucination_pulse(src, HALLUCINATION_HFR(heat_output), 100 SECONDS * power_level * seconds_per_tick)
+
+ if(5)
+ if(moderator_list[GAS_PLASMA] > 15)
+ internal_output.adjust_moles(GAS_FREON, scaled_production * 0.25)
+ moderator_internal.adjust_moles(GAS_PLASMA, -min(moderator_internal.get_moles(GAS_PLASMA), scaled_production * 1.45))
+ if(moderator_list[GAS_FREON] > 500)
+ heat_output *= 0.5
+ radiation *= 0.2
+ if(moderator_list[GAS_PROTO_NITRATE] > 50)
+ internal_output.adjust_moles(GAS_NITRIUM, scaled_production * 1.95)
+ internal_output.adjust_moles(GAS_PLUOXIUM, scaled_production)
+ moderator_internal.adjust_moles(GAS_PROTO_NITRATE, -min(moderator_internal.get_moles(GAS_PROTO_NITRATE), scaled_production * 1.35))
+ radiation *= 1.95
+ heat_output *= 1.25
+ if(moderator_list[GAS_BZ] > 100)
+ internal_output.adjust_moles(GAS_HEALIUM, scaled_production)
+ visible_hallucination_pulse(src, HALLUCINATION_HFR(heat_output), 100 SECONDS * power_level * seconds_per_tick)
+ internal_output.adjust_moles(GAS_FREON, scaled_production * 1.15)
+ if(moderator_list[GAS_HEALIUM] > 100)
+ if(critical_threshold_proximity > HFR_HEALIUM_HEAL_PROXIMITY_THRESHOLD)
+ critical_threshold_proximity = max(critical_threshold_proximity - (moderator_list[GAS_HEALIUM] / 100) * HFR_HEALIUM_HEAL_RATE_FACTOR * melting_point * seconds_per_tick, 0)
+ moderator_internal.adjust_moles(GAS_HEALIUM, -min(moderator_internal.get_moles(GAS_HEALIUM), scaled_production * 20))
+ if(moderator_internal.return_temperature() < HFR_ANTINOBLIUM_TEMP_THRESHOLD || (moderator_list[GAS_PLASMA] > 100 && moderator_list[GAS_BZ] > 50))
+ internal_output.adjust_moles(GAS_ANTINOBLIUM, dirty_production_rate * 0.9 / 0.065 * seconds_per_tick)
+ if(6)
+ if(moderator_list[GAS_PLASMA] > 30)
+ internal_output.adjust_moles(GAS_BZ, scaled_production * 1.15)
+ moderator_internal.adjust_moles(GAS_PLASMA, -min(moderator_internal.get_moles(GAS_PLASMA), scaled_production * 1.45))
+ if(moderator_list[GAS_PROTO_NITRATE])
+ internal_output.adjust_moles(GAS_ZAUKER, scaled_production * 5.35)
+ internal_output.adjust_moles(GAS_NITRIUM, scaled_production * 2.15)
+ moderator_internal.adjust_moles(GAS_PROTO_NITRATE, -min(moderator_internal.get_moles(GAS_PROTO_NITRATE), scaled_production * 3.35))
+ radiation *= 2
+ heat_output *= 2.25
+ if(moderator_list[GAS_BZ])
+ visible_hallucination_pulse(src, HALLUCINATION_HFR(heat_output), 100 SECONDS * power_level * seconds_per_tick)
+ internal_output.adjust_moles(GAS_ANTINOBLIUM, clamp(dirty_production_rate / 0.045, 0, 10) * seconds_per_tick)
+ if(moderator_list[GAS_HEALIUM] > 100)
+ if(critical_threshold_proximity > HFR_HEALIUM_HEAL_PROXIMITY_THRESHOLD)
+ critical_threshold_proximity = max(critical_threshold_proximity - (moderator_list[GAS_HEALIUM] / 100) * HFR_HEALIUM_HEAL_RATE_FACTOR * melting_point * seconds_per_tick, 0)
+ moderator_internal.adjust_moles(GAS_HEALIUM, -min(moderator_internal.get_moles(GAS_HEALIUM), scaled_production * 20))
+ internal_fusion.adjust_moles(GAS_ANTINOBLIUM, dirty_production_rate * 0.01 / 0.095 * seconds_per_tick)
+
+ // Температура fusion: если не перегрев, добавляем heat_output за тик и clamp; иначе охлаждаем на heat_limiter_modifier за тик.
+ if(internal_fusion.return_temperature() <= FUSION_MAXIMUM_TEMPERATURE)
+ internal_fusion.set_temperature(clamp(
+ internal_fusion.return_temperature() + heat_output * seconds_per_tick,
+ TCMB,
+ FUSION_MAXIMUM_TEMPERATURE,
+ ))
+ else
+ internal_fusion.set_temperature(internal_fusion.return_temperature() - heat_limiter_modifier * HFR_COOLING_PER_TICK_FACTOR * seconds_per_tick)
+
+ // Температура выхода: от модератора или от fusion. Мержим в linked_output и чистим кэш.
+ if(hfr_internal_output.total_moles() > 0)
+ if(moderator_internal.total_moles() > 0)
+ hfr_internal_output.set_temperature(moderator_internal.return_temperature() * HIGH_EFFICIENCY_CONDUCTIVITY)
+ else
+ hfr_internal_output.set_temperature(internal_fusion.return_temperature() * METALLIC_VOID_CONDUCTIVITY)
+ linked_output.airs[1].merge(hfr_internal_output)
+ hfr_internal_output.clear()
+
+ evaporate_moderator(seconds_per_tick)
+
+ check_nuclear_particles(hfr_moderator_list)
+
+ check_lightning_arcs(hfr_moderator_list)
+
+ // Хил железа кислородом: при достаточном O2 в модераторе убавляем iron_content и тратим O2 по константам.
+ if(hfr_moderator_list[GAS_O2] > HFR_IRON_HEAL_O2_THRESHOLD)
+ if(iron_content > 0)
+ var/max_iron_removable = IRON_OXYGEN_HEAL_PER_SECOND
+ var/iron_removed = min(max_iron_removable * seconds_per_tick, iron_content)
+ iron_content -= iron_removed
+ moderator_internal.adjust_moles(GAS_O2, -iron_removed * OXYGEN_MOLES_CONSUMED_PER_IRON_HEAL)
+
+ check_gravity_pulse(seconds_per_tick)
+
+ radiation_pulse(src, 500, HFR_LIGHTNING_RADIATION_RANGE)
+
+/// Удаляет из модератора долю молей за тик (экспонента от уровня). Чем выше уровень, тем быстрее испарение.
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/evaporate_moderator(seconds_per_tick)
+ if (!power_level)
+ return
+ if(moderator_internal.total_moles() > 0)
+ moderator_internal.remove(moderator_internal.total_moles() * (1 - (1 - HFR_EVAPORATE_RATE_BASE * power_level) ** seconds_per_tick))
+
+/// Целостность (critical_threshold_proximity): урон от переполнения, температуры, железа, overmole; хил от малой массы, холодного куланта, кислорода. В конце cap по DAMAGE_CAP_MULTIPLIER.
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/process_damageheal(seconds_per_tick)
+ critical_threshold_proximity_archived = critical_threshold_proximity
+
+ warning_damage_flags &= HYPERTORUS_FLAG_EMPED
+
+ // Урон при переполнении (много молей и/или высокая темп) на высоком уровне. Плюс урон от лога температуры.
+ if(power_level >= HYPERTORUS_OVERFULL_MIN_POWER_LEVEL)
+ var/fusion_temp = internal_fusion.return_temperature()
+ var/overfull_damage_taken = HYPERTORUS_OVERFULL_MOLAR_SLOPE * internal_fusion.total_moles() + HYPERTORUS_OVERFULL_TEMPERATURE_SLOPE * fusion_temp + HYPERTORUS_OVERFULL_CONSTANT
+ critical_threshold_proximity = max(critical_threshold_proximity + max(overfull_damage_taken * seconds_per_tick, 0), 0)
+ warning_damage_flags |= HYPERTORUS_FLAG_HIGH_POWER_DAMAGE
+ // High fusion temperature damage: log10(fusion_temp) - 5 per tick (doc: 2 at 5e7K, 1 at 1e6K)
+ var/high_temp_damage = log(10, max(fusion_temp, 1)) - 5
+ critical_threshold_proximity = max(critical_threshold_proximity + max(high_temp_damage * seconds_per_tick, 0), 0)
+
+ // Хил при малой массе в смеси (ниже порога) на уровне не выше 4.
+ if(internal_fusion.total_moles() < HYPERTORUS_SUBCRITICAL_MOLES && power_level <= 4)
+ var/subcritical_heal_restore = (internal_fusion.total_moles() - HYPERTORUS_SUBCRITICAL_MOLES) / HYPERTORUS_SUBCRITICAL_SCALE
+ critical_threshold_proximity = max(critical_threshold_proximity + min(subcritical_heal_restore * seconds_per_tick, 0), 0)
+
+ // Хил от холодного куланта: температура куланта ниже порога, лог даёт отрицательный restore, min(...,0) убавляет proximity.
+ if(internal_fusion.total_moles() > 0 && (airs[1].total_moles() && coolant_temperature < HYPERTORUS_COLD_COOLANT_THRESHOLD) && power_level <= 4)
+ var/cold_coolant_heal_restore = log(10, max(coolant_temperature, 1) * HYPERTORUS_COLD_COOLANT_SCALE) - (HYPERTORUS_COLD_COOLANT_MAX_RESTORE * 2)
+ critical_threshold_proximity = max(critical_threshold_proximity + min(cold_coolant_heal_restore * seconds_per_tick, 0), 0)
+
+ // Урон от железа: (iron_content - 1) * коэффициент за тик. Потом общий cap роста за тик.
+ critical_threshold_proximity += max(round(iron_content) - 1, 0) * HFR_IRON_DAMAGE_PER_POINT * seconds_per_tick
+ if(round(iron_content) > 1)
+ warning_damage_flags |= HYPERTORUS_FLAG_IRON_CONTENT_DAMAGE
+
+ critical_threshold_proximity = min(critical_threshold_proximity_archived + (seconds_per_tick * DAMAGE_CAP_MULTIPLIER * melting_point), critical_threshold_proximity)
+
+ // Гиперкритический урон: молей выше порога, прирост ограничен HYPERTORUS_HYPERCRITICAL_MAX_DAMAGE за тик.
+ if(internal_fusion.total_moles() >= HYPERTORUS_HYPERCRITICAL_MOLES)
+ var/hypercritical_damage_taken = max((internal_fusion.total_moles() - HYPERTORUS_HYPERCRITICAL_MOLES) * HYPERTORUS_HYPERCRITICAL_SCALE, 0)
+ var/clamped_increment = min(hypercritical_damage_taken, HYPERTORUS_HYPERCRITICAL_MAX_DAMAGE) * seconds_per_tick
+ critical_threshold_proximity = max(critical_threshold_proximity + clamped_increment, 0)
+ warning_damage_flags |= HYPERTORUS_FLAG_HIGH_FUEL_MIX_MOLE
+
+ // Over HFR_OVERMOLE_MOLES: lose HFR_OVERMOLE_DAMAGE_FRAC integrity every HFR_OVERMOLE_INTERVAL_DS ds, capped so large melting_point doesn't overshoot
+ if(internal_fusion.total_moles() > HFR_OVERMOLE_MOLES && (world.time - last_overmole_damage) >= HFR_OVERMOLE_INTERVAL_DS)
+ var/overmole_cap = 10 * seconds_per_tick * DAMAGE_CAP_MULTIPLIER * melting_point
+ critical_threshold_proximity += min(melting_point * HFR_OVERMOLE_DAMAGE_FRAC, overmole_cap, HYPERTORUS_OVERMOLE_MAX_ADD)
+ critical_threshold_proximity = min(critical_threshold_proximity_archived + overmole_cap, critical_threshold_proximity)
+ last_overmole_damage = world.time
+
+ // Железо: на уровне >4 с вероятностью растёт; на уровне <=4 с вероятностью падает. Потом clamp в [0, max].
+ if(power_level > 4 && prob(IRON_CHANCE_PER_FUSION_LEVEL * power_level))
+ iron_content += IRON_ACCUMULATED_PER_SECOND * seconds_per_tick
+ warning_damage_flags |= HYPERTORUS_FLAG_IRON_CONTENT_INCREASE
+ if(iron_content > 0 && power_level <= 4 && prob(HFR_IRON_HEAL_CHANCE_DIVISOR / (power_level + 1)))
+ iron_content = max(iron_content - HFR_IRON_DECAY_RATE * seconds_per_tick, 0)
+ iron_content = clamp(iron_content, 0, HFR_IRON_CONTENT_MAX)
+
+/// При уровне >= 4 и достаточном BZ стреляет ядерной частицей из случайного угла в противоположную сторону.
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_nuclear_particles(moderator_list)
+ if(power_level < 4)
+ return
+ if(moderator_list[GAS_BZ] < (150 / power_level))
+ return
+ var/obj/machinery/hypertorus/corner/picked_corner = pick(corners)
+ picked_corner.loc.fire_nuclear_particle(REVERSE_DIR(picked_corner.dir))
+
+/// При уровне >= 4, достаточном Antinoblium или proximity запускает tesla_zap. Количество разрядов и флаги урона зависят от power_level и proximity.
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_lightning_arcs(moderator_list)
+ if(power_level < 4)
+ return
+ if(moderator_list[GAS_ANTINOBLIUM] <= HFR_LIGHTNING_ANTINOBLIUM_MIN && critical_threshold_proximity <= HFR_LIGHTNING_PROXIMITY_MAX)
+ return
+ var/zap_number = power_level - 2
+
+ if(critical_threshold_proximity > 650 && prob(20))
+ zap_number += 1
+
+ var/flags = ZAP_SUPERMATTER_FLAGS
+ switch(power_level)
+ if(5)
+ flags |= (ZAP_MOB_DAMAGE)
+ if(6)
+ flags |= (ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE)
+
+ playsound(loc, 'sound/weapons/emitter2.ogg', 100, TRUE, extrarange = 10)
+ for(var/i in 1 to zap_number)
+ tesla_zap(src, 5, power_level * 2.4e5, flags)
+
+/// С вероятностью от proximity тянет мобов в радиусе grav_range к реактору. Радиус от log(2.5, proximity).
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_gravity_pulse(seconds_per_tick)
+ if(SPT_PROB(100 - critical_threshold_proximity / 15, seconds_per_tick))
+ return
+ var/grav_range = round(log(2.5, critical_threshold_proximity))
+ for(var/mob/alive_mob in view(grav_range, src))
+ if(alive_mob.mob_negates_gravity())
+ continue
+ step_towards(alive_mob, loc)
+
+/// Сливает в output отфильтрованные газы модератора и часть He/Antinoblium из fusion. Вызывается при waste_remove и уровне < 6.
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/remove_waste(seconds_per_tick)
+ if(!waste_remove)
+ return
+ // Forcibly disabled at fusion power level 6
+ if(power_level >= 6)
+ return
+ var/filtering_amount = moderator_scrubbing.len
+ for(var/gas_id in moderator_internal.get_gases() & moderator_scrubbing)
+ var/datum/gas_mixture/removed = moderator_internal.remove_specific(gas_id, (moderator_filtering_rate / filtering_amount) * seconds_per_tick, hfr_removed_waste)
+ if(removed)
+ linked_output.airs[1].merge(removed)
+ hfr_removed_waste.clear()
+
+ // 50% of Fusion Mix Helium per second, 5% of Fusion Mix Anti-Noblium per second
+ if(internal_fusion.get_moles(GAS_HELIUM) > 0)
+ var/datum/gas_mixture/removed = internal_fusion.remove_specific(GAS_HELIUM, internal_fusion.get_moles(GAS_HELIUM) * (1 - (1 - 0.5) ** seconds_per_tick), hfr_removed_waste)
+ if(removed)
+ linked_output.airs[1].merge(removed)
+ hfr_removed_waste.clear()
+ if(internal_fusion.get_moles(GAS_ANTINOBLIUM) > 0)
+ var/datum/gas_mixture/removed = internal_fusion.remove_specific(GAS_ANTINOBLIUM, internal_fusion.get_moles(GAS_ANTINOBLIUM) * (1 - (1 - 0.05) ** seconds_per_tick), hfr_removed_waste)
+ if(removed)
+ linked_output.airs[1].merge(removed)
+ hfr_removed_waste.clear()
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/process_internal_cooling(seconds_per_tick)
+ if(moderator_internal.total_moles() > 0 && internal_fusion.total_moles() > 0)
+ var/fusion_temperature_delta = internal_fusion.return_temperature() - moderator_internal.return_temperature()
+ var/fusion_heat_amount = (1 - (1 - METALLIC_VOID_CONDUCTIVITY) ** seconds_per_tick) * fusion_temperature_delta * (internal_fusion.heat_capacity() * moderator_internal.heat_capacity() / (internal_fusion.heat_capacity() + moderator_internal.heat_capacity()))
+ internal_fusion.set_temperature(max(internal_fusion.return_temperature() - fusion_heat_amount / internal_fusion.heat_capacity(), TCMB))
+ moderator_internal.set_temperature(max(moderator_internal.return_temperature() + fusion_heat_amount / moderator_internal.heat_capacity(), TCMB))
+
+ if(airs[1].total_moles() * 0.05 <= MINIMUM_MOLE_COUNT)
+ return
+ var/datum/gas_mixture/cooling_port = airs[1]
+ var/datum/gas_mixture/cooling_remove = cooling_port.remove(0.05 * cooling_port.total_moles())
+ if(moderator_internal.total_moles() > 0)
+ var/coolant_temperature_delta = cooling_remove.return_temperature() - moderator_internal.return_temperature()
+ var/cooling_heat_amount = (1 - (1 - HIGH_EFFICIENCY_CONDUCTIVITY) ** seconds_per_tick) * coolant_temperature_delta * (cooling_remove.heat_capacity() * moderator_internal.heat_capacity() / (cooling_remove.heat_capacity() + moderator_internal.heat_capacity()))
+ cooling_remove.set_temperature(max(cooling_remove.return_temperature() - cooling_heat_amount / cooling_remove.heat_capacity(), TCMB))
+ moderator_internal.set_temperature(max(moderator_internal.return_temperature() + cooling_heat_amount / moderator_internal.heat_capacity(), TCMB))
+
+ else if(internal_fusion.total_moles() > 0)
+ var/coolant_temperature_delta = cooling_remove.return_temperature() - internal_fusion.return_temperature()
+ var/cooling_heat_amount = (1 - (1 - METALLIC_VOID_CONDUCTIVITY) ** seconds_per_tick) * coolant_temperature_delta * (cooling_remove.heat_capacity() * internal_fusion.heat_capacity() / (cooling_remove.heat_capacity() + internal_fusion.heat_capacity()))
+ cooling_remove.set_temperature(max(cooling_remove.return_temperature() - cooling_heat_amount / cooling_remove.heat_capacity(), TCMB))
+ internal_fusion.set_temperature(max(internal_fusion.return_temperature() + cooling_heat_amount / internal_fusion.heat_capacity(), TCMB))
+ cooling_port.merge(cooling_remove)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/inject_from_side_components(seconds_per_tick)
+ update_pipenets()
+
+ var/datum/gas_mixture/moderator_port = linked_moderator.airs[1]
+ if(start_moderator && moderator_port.total_moles())
+ moderator_internal.merge(moderator_port.remove(moderator_injection_rate * seconds_per_tick))
+ linked_moderator.update_parents()
+
+ if(!start_fuel || !selected_fuel || !check_gas_requirements())
+ return
+
+ var/datum/gas_mixture/fuel_port = linked_input.airs[1]
+ for(var/gas_type in selected_fuel.requirements)
+ var/datum/gas_mixture/removed = fuel_port.remove_specific(gas_type, fuel_injection_rate * seconds_per_tick / length(selected_fuel.requirements), hfr_removed_waste)
+ if(removed)
+ internal_fusion.merge(removed)
+ hfr_removed_waste.clear()
+ linked_input.update_parents()
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_deconstructable()
+ if(!active)
+ return
+ if(power_level > 0)
+ fusion_started = TRUE
+ linked_input.fusion_started = TRUE
+ linked_output.fusion_started = TRUE
+ linked_moderator.fusion_started = TRUE
+ linked_interface.fusion_started = TRUE
+ for(var/obj/machinery/hypertorus/corner/corner in corners)
+ corner.fusion_started = TRUE
+ else
+ fusion_started = FALSE
+ linked_input.fusion_started = FALSE
+ linked_output.fusion_started = FALSE
+ linked_moderator.fusion_started = FALSE
+ linked_interface.fusion_started = FALSE
+ for(var/obj/machinery/hypertorus/corner/corner in corners)
+ corner.fusion_started = FALSE
diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_parts.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_parts.dm
new file mode 100644
index 0000000000000..3531cddf17d36
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/fusion/hfr_parts.dm
@@ -0,0 +1,531 @@
+/**
+ * This file contain the eight parts surrounding the main core, those are: fuel input, moderator input, waste output, interface and the corners
+ * The file also contain the guicode of the machine
+ * BlueMoon: gas ID strings (GAS_O2, GAS_PLASMA, etc.), get_gases/get_moles, GLOB.gas_data.
+ */
+/obj/machinery/atmospherics/components/unary/hypertorus
+ icon = 'icons/obj/machines/atmospherics/hypertorus.dmi'
+ icon_state = "core_off"
+
+ name = "thermomachine"
+ desc = "Heats or cools gas in connected pipes."
+ anchored = TRUE
+ density = TRUE
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF
+ flags_1 = PREVENT_CONTENTS_EXPLOSION_1
+ layer = OBJ_LAYER
+ pipe_flags = PIPING_ONE_PER_TURF | PIPING_DEFAULT_LAYER_ONLY
+ circuit = /obj/item/circuitboard/machine/thermomachine
+ ///Vars for the state of the icon of the object (open, off, active)
+ var/icon_state_open
+ var/icon_state_off
+ var/icon_state_active
+ ///Check if the machine has been activated
+ var/active = FALSE
+ ///Check if fusion has started
+ var/fusion_started = FALSE
+ ///Check if the machine is cracked open
+ var/cracked = FALSE
+
+/obj/machinery/atmospherics/components/unary/hypertorus/Initialize(mapload)
+ . = ..()
+ initialize_directions = dir
+
+/obj/machinery/atmospherics/components/unary/hypertorus/examine(mob/user)
+ . = ..()
+ . += span_notice("[src] can be rotated by first opening the panel with a screwdriver and then using a wrench on it.")
+
+/obj/machinery/atmospherics/components/unary/hypertorus/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers)
+ if(!fusion_started)
+ if(default_deconstruction_screwdriver(user, icon_state_open, icon_state_off, I))
+ return
+ if(default_change_direction_wrench(user, I))
+ return
+ return ..()
+
+/obj/machinery/atmospherics/components/unary/hypertorus/welder_act(mob/living/user, obj/item/tool)
+ if(!cracked)
+ return FALSE
+ if(user.combat_mode)
+ return FALSE
+ balloon_alert(user, "repairing...")
+ if(tool.use_tool(src, user, 10 SECONDS, volume=30))
+ balloon_alert(user, "repaired")
+ cracked = FALSE
+ update_appearance(UPDATE_ICON)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/crowbar_act(mob/living/user, obj/item/tool)
+ return default_deconstruction_crowbar(tool)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/update_icon_state()
+ if(panel_open)
+ icon_state = icon_state_open
+ return ..()
+ if(active)
+ icon_state = icon_state_active
+ return ..()
+ icon_state = icon_state_off
+ return ..()
+
+/obj/machinery/atmospherics/components/unary/hypertorus/update_overlays()
+ . = ..()
+ if(!cracked)
+ return
+ var/image/crack = image(icon, icon_state = "crack")
+ crack.dir = dir
+ . += crack
+
+/obj/machinery/atmospherics/components/unary/hypertorus/update_layer()
+ return
+
+/obj/machinery/atmospherics/components/unary/hypertorus/fuel_input
+ name = "HFR fuel input port"
+ desc = "Input port for the Hypertorus Fusion Reactor, designed to take in fuels with the optimal fuel mix being a 50/50 split."
+ icon_state = "fuel_input_off"
+ icon_state_open = "fuel_input_open"
+ icon_state_off = "fuel_input_off"
+ icon_state_active = "fuel_input_active"
+ circuit = /obj/item/circuitboard/machine/HFR_fuel_input
+
+/obj/machinery/atmospherics/components/unary/hypertorus/waste_output
+ name = "HFR waste output port"
+ desc = "Waste port for the Hypertorus Fusion Reactor, designed to output the hot waste gases coming from the core of the machine."
+ icon_state = "waste_output_off"
+ icon_state_open = "waste_output_open"
+ icon_state_off = "waste_output_off"
+ icon_state_active = "waste_output_active"
+ circuit = /obj/item/circuitboard/machine/HFR_waste_output
+
+/obj/machinery/atmospherics/components/unary/hypertorus/moderator_input
+ name = "HFR moderator input port"
+ desc = "Moderator port for the Hypertorus Fusion Reactor, designed to move gases inside the machine to cool and control the flow of the reaction."
+ icon_state = "moderator_input_off"
+ icon_state_open = "moderator_input_open"
+ icon_state_off = "moderator_input_off"
+ icon_state_active = "moderator_input_active"
+ circuit = /obj/item/circuitboard/machine/HFR_moderator_input
+
+/*
+* Interface and corners
+*/
+/obj/machinery/hypertorus
+ name = "hypertorus_core"
+ desc = "hypertorus_core"
+ icon = 'icons/obj/machines/atmospherics/hypertorus.dmi'
+ icon_state = "core_off"
+ move_resist = INFINITY
+ anchored = TRUE
+ density = TRUE
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF
+ flags_1 = PREVENT_CONTENTS_EXPLOSION_1
+ power_channel = AREA_USAGE_ENVIRON
+ var/active = FALSE
+ var/icon_state_open
+ var/icon_state_off
+ var/icon_state_active
+ var/fusion_started = FALSE
+
+/obj/machinery/hypertorus/examine(mob/user)
+ . = ..()
+ . += span_notice("[src] can be rotated by first opening the panel with a screwdriver and then using a wrench on it.")
+
+/obj/machinery/hypertorus/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers)
+ if(!fusion_started)
+ if(default_deconstruction_screwdriver(user, icon_state_open, icon_state_off, I))
+ return
+ if(default_change_direction_wrench(user, I))
+ return
+ if(default_deconstruction_crowbar(I))
+ return
+ return ..()
+
+/obj/machinery/hypertorus/update_icon_state()
+ if(panel_open)
+ icon_state = icon_state_open
+ return ..()
+ if(active)
+ icon_state = icon_state_active
+ return ..()
+ icon_state = icon_state_off
+ return ..()
+
+/obj/machinery/hypertorus/interface
+ name = "HFR interface"
+ desc = "Interface for the HFR to control the flow of the reaction."
+ icon_state = "interface_off"
+ circuit = /obj/item/circuitboard/machine/HFR_interface
+ icon_state_off = "interface_off"
+ icon_state_open = "interface_open"
+ icon_state_active = "interface_active"
+ /// Have we been activated at least once?
+ var/activated = FALSE
+ /// Reference to the core of our machine
+ var/obj/machinery/atmospherics/components/unary/hypertorus/core/connected_core
+
+/obj/machinery/hypertorus/interface/Destroy()
+ if(connected_core)
+ connected_core = null
+ return ..()
+
+/obj/machinery/hypertorus/interface/multitool_act(mob/living/user, obj/item/I)
+ . = ..()
+ var/turf/T = get_step(src,REVERSE_DIR(dir))
+ var/obj/machinery/atmospherics/components/unary/hypertorus/core/centre = locate() in T
+
+ if(!centre || !centre.check_part_connectivity())
+ to_chat(user, span_notice("Check all parts and then try again."))
+ return TRUE
+
+ connected_core = centre
+ connected_core.activate(user)
+ if(!activated)
+ new /obj/item/paper/guides/jobs/atmos/hypertorus(loc)
+ activated = TRUE
+
+ return TRUE
+
+/obj/machinery/hypertorus/interface/ui_interact(mob/user, datum/tgui/ui)
+ if(active)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "Hypertorus", name)
+ ui.open()
+ else
+ to_chat(user, span_notice("Activate the machine first by using a multitool on the interface."))
+ if(ui)
+ ui.close()
+
+/// BlueMoon: requirements/primary_products/secondary_products are already gas id lists; return copy.
+/obj/machinery/hypertorus/interface/proc/gas_list_to_gasid_list(list/gas_list)
+ return gas_list.Copy()
+
+/obj/machinery/hypertorus/interface/ui_static_data()
+ var/data = list()
+ data["base_max_temperature"] = FUSION_MAXIMUM_TEMPERATURE
+ data["selectable_fuel"] = list(list("name" = "Nothing", "id" = null))
+ for(var/fuel_id in GLOB.hfr_fuels_list)
+ var/datum/hfr_fuel/recipe = GLOB.hfr_fuels_list[fuel_id]
+ data["selectable_fuel"] += list(list(
+ "name" = recipe.name,
+ "id" = recipe.id,
+ "requirements" = recipe.requirements,
+ "fusion_byproducts" = recipe.primary_products,
+ "product_gases" = recipe.secondary_products,
+ "recipe_cooling_multiplier" = recipe.negative_temperature_multiplier,
+ "recipe_heating_multiplier" = recipe.positive_temperature_multiplier,
+ "energy_loss_multiplier" = recipe.energy_concentration_multiplier,
+ "fuel_consumption_multiplier" = recipe.fuel_consumption_multiplier,
+ "gas_production_multiplier" = recipe.gas_production_multiplier,
+ "temperature_multiplier" = recipe.temperature_change_multiplier,
+ ))
+ return data
+
+/obj/machinery/hypertorus/interface/ui_data()
+ var/data = list()
+ if(!connected_core)
+ return data
+
+ if(connected_core.selected_fuel)
+ data["selected"] = connected_core.selected_fuel.id
+ else
+ data["selected"] = null
+
+ // Product gases (moderator output) for selected reaction
+ var/list/product_names = list()
+ if(connected_core.selected_fuel)
+ for(var/gas_id in connected_core.selected_fuel.secondary_products)
+ product_names += GLOB.gas_data.names[gas_id]
+ data["product_gases"] = product_names.len ? product_names.Join(", ") : "None"
+
+ //Internal Fusion gases - BlueMoon: get_gases(), get_moles(gas_id)
+ var/list/fusion_gasdata = list()
+ if(connected_core.internal_fusion.total_moles())
+ for(var/gas_id in connected_core.internal_fusion.get_gases())
+ fusion_gasdata.Add(list(list(
+ "id" = gas_id,
+ "amount" = round(connected_core.internal_fusion.get_moles(gas_id), 0.01),
+ )))
+ else
+ for(var/gas_id in GLOB.gas_data.ids)
+ fusion_gasdata.Add(list(list(
+ "id" = gas_id,
+ "amount" = 0,
+ )))
+ //Moderator gases
+ var/list/moderator_gasdata = list()
+ if(connected_core.moderator_internal.total_moles())
+ for(var/gas_id in connected_core.moderator_internal.get_gases())
+ moderator_gasdata.Add(list(list(
+ "id" = gas_id,
+ "amount" = round(connected_core.moderator_internal.get_moles(gas_id), 0.01),
+ )))
+ else
+ for(var/gas_id in GLOB.gas_data.ids)
+ moderator_gasdata.Add(list(list(
+ "id" = gas_id,
+ "amount" = 0,
+ )))
+
+ data["fusion_gases"] = fusion_gasdata
+ data["moderator_gases"] = moderator_gasdata
+
+ data["energy_level"] = connected_core.energy
+ data["heat_limiter_modifier"] = connected_core.heat_limiter_modifier
+ data["heat_output_min"] = connected_core.heat_output_min
+ data["heat_output_max"] = connected_core.heat_output_max
+ data["heat_output"] = connected_core.heat_output
+ data["instability"] = connected_core.instability
+
+ data["heating_conductor"] = connected_core.heating_conductor
+ data["magnetic_constrictor"] = connected_core.magnetic_constrictor
+ data["fuel_injection_rate"] = connected_core.fuel_injection_rate
+ data["moderator_injection_rate"] = connected_core.moderator_injection_rate
+ data["current_damper"] = connected_core.current_damper
+
+ data["power_level"] = connected_core.power_level
+ data["apc_energy"] = connected_core.get_area_cell_percent()
+ data["iron_content"] = connected_core.iron_content
+ data["integrity"] = connected_core.get_integrity_percent()
+
+ data["start_power"] = connected_core.start_power
+ data["start_cooling"] = connected_core.start_cooling
+ data["start_fuel"] = connected_core.start_fuel
+ data["start_moderator"] = connected_core.start_moderator
+
+ data["internal_fusion_temperature"] = connected_core.fusion_temperature
+ data["moderator_internal_temperature"] = connected_core.moderator_temperature
+ data["internal_output_temperature"] = connected_core.output_temperature
+ data["internal_coolant_temperature"] = connected_core.coolant_temperature
+
+ data["internal_fusion_temperature_archived"] = connected_core.fusion_temperature_archived
+ data["moderator_internal_temperature_archived"] = connected_core.moderator_temperature_archived
+ data["internal_output_temperature_archived"] = connected_core.output_temperature_archived
+ data["internal_coolant_temperature_archived"] = connected_core.coolant_temperature_archived
+ data["temperature_period"] = connected_core.temperature_period
+
+ data["waste_remove"] = connected_core.waste_remove
+ data["filter_types"] = list()
+ for(var/id in GLOB.gas_data.ids)
+ data["filter_types"] += list(list("gas_id" = id, "gas_name" = GLOB.gas_data.names[id], "enabled" = (id in connected_core.moderator_scrubbing)))
+
+ data["cooling_volume"] = connected_core.airs[1].return_volume()
+ data["mod_filtering_rate"] = connected_core.moderator_filtering_rate
+
+ return data
+
+/obj/machinery/hypertorus/interface/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ if(!connected_core)
+ return
+ switch(action)
+ if("start_power")
+ connected_core.start_power = !connected_core.start_power
+ connected_core.use_power = connected_core.start_power ? ACTIVE_POWER_USE : IDLE_POWER_USE
+ . = TRUE
+ if("start_cooling")
+ connected_core.start_cooling = !connected_core.start_cooling
+ . = TRUE
+ if("start_fuel")
+ connected_core.start_fuel = !connected_core.start_fuel
+ . = TRUE
+ if("start_moderator")
+ connected_core.start_moderator = !connected_core.start_moderator
+ . = TRUE
+ if("heating_conductor")
+ var/heating_conductor = text2num(params["heating_conductor"])
+ if(heating_conductor != null)
+ connected_core.heating_conductor = clamp(heating_conductor, 50, 500)
+ . = TRUE
+ if("magnetic_constrictor")
+ var/magnetic_constrictor = text2num(params["magnetic_constrictor"])
+ if(magnetic_constrictor != null)
+ connected_core.magnetic_constrictor = clamp(magnetic_constrictor, 50, 1000)
+ . = TRUE
+ if("fuel_injection_rate")
+ var/fuel_injection_rate = text2num(params["fuel_injection_rate"])
+ if(fuel_injection_rate != null)
+ connected_core.fuel_injection_rate = clamp(fuel_injection_rate, 5, 1500)
+ . = TRUE
+ if("moderator_injection_rate")
+ var/moderator_injection_rate = text2num(params["moderator_injection_rate"])
+ if(moderator_injection_rate != null)
+ connected_core.moderator_injection_rate = clamp(moderator_injection_rate, 5, 1500)
+ . = TRUE
+ if("current_damper")
+ var/current_damper = text2num(params["current_damper"])
+ if(current_damper != null)
+ connected_core.current_damper = clamp(current_damper, 0, 1000)
+ . = TRUE
+ if("waste_remove")
+ connected_core.waste_remove = !connected_core.waste_remove
+ . = TRUE
+ if("filter")
+ connected_core.moderator_scrubbing ^= params["mode"]
+ . = TRUE
+ if("mod_filtering_rate")
+ var/mod_filtering_rate = text2num(params["mod_filtering_rate"])
+ if(mod_filtering_rate != null)
+ connected_core.moderator_filtering_rate = clamp(mod_filtering_rate, 5, 200)
+ . = TRUE
+ if("fuel")
+ connected_core.selected_fuel = null
+ var/fuel_mix = "nothing"
+ var/datum/hfr_fuel/fuel = null
+ if(params["mode"] != "")
+ fuel = GLOB.hfr_fuels_list[params["mode"]]
+ if(fuel)
+ connected_core.selected_fuel = fuel
+ fuel_mix = fuel.name
+ if(connected_core.internal_fusion.total_moles())
+ connected_core.dump_gases()
+ connected_core.update_parents()
+ connected_core.linked_input.update_parents()
+ connected_core.linked_output.update_parents()
+ connected_core.linked_moderator.update_parents()
+ investigate_log("was set to recipe [fuel_mix ? fuel_mix : "null"] by [key_name(usr)]", INVESTIGATE_ATMOS)
+ . = TRUE
+ if("cooling_volume")
+ var/cooling_volume = text2num(params["cooling_volume"])
+ if(cooling_volume != null)
+ connected_core.airs[1].set_volume(clamp(cooling_volume, 50, 2000))
+ . = TRUE
+
+/obj/machinery/hypertorus/corner
+ name = "HFR corner"
+ desc = "Structural piece of the machine."
+ icon_state = "corner_off"
+ circuit = /obj/item/circuitboard/machine/HFR_corner
+ icon_state_off = "corner_off"
+ icon_state_open = "corner_open"
+ icon_state_active = "corner_active"
+
+/obj/item/paper/guides/jobs/atmos/hypertorus
+ name = "paper- 'Quick guide to safe handling of the HFR'"
+ default_raw_text = "How to safely(TM) operate the Hypertorus \
+ -Build the machine as its shown in the main guide. \
+ -Make a 50/50 gasmix of tritium and hydrogen totalling around 2000 moles. \
+ -Start the machine, fill up the cooling loop with plasma/hypernoblium and use space or freezers to cool it. \
+ -Connect the fuel mix into the fuel injector port, allow only 1000 moles into the machine to ease the kickstart of the reaction \
+ -Set the Heat conductor to 500 when starting the reaction, reset it to 100 when power level is higher than 1 \
+ -In the event of a meltdown, set the heat conductor to max and set the current damper to max. Set the fuel injection to min. \
+ If the heat output doesnt go negative, try changing the magnetic costrictors untill heat output goes negative. \
+ Make the cooling stronger, put high heat capacity gases inside the moderator (hypernoblium will help dealing with the problem) \
+ Warnings: \
+ -You cannot dismantle the machine if the power level is over 0 \
+ -You cannot power of the machine if the power level is over 0 \
+ -You cannot dispose of waste gases if power level is over 5 \
+ -You cannot remove gases from the fusion mix if they are not helium and antinoblium \
+ -Hypernoblium will decrease the power of the mix by a lot \
+ -Antinoblium will INCREASE the power of the mix by a lot more \
+ -High heat capacity gases are harder to heat/cool \
+ -Low heat capacity gases are easier to heat/cool \
+ -The machine consumes 50 KW per power level, reaching 350 KW at power level 6 so prepare the SM accordingly \
+ -In case of a power shortage, the fusion reaction will CONTINUE but the cooling will STOP \
+ The writer of the quick guide will not be held responsible for misuses and meltdown caused by the use of the guide, \
+ use more advanced guides to understando how the various gases will act as moderators."
+
+/obj/item/hfr_box
+ name = "HFR box"
+ desc = "If you see this, call the police."
+ icon = 'icons/obj/machines/atmospherics/hypertorus.dmi'
+ icon_state = "error"
+ ///What kind of box are we handling?
+ var/box_type = "impossible"
+ ///What's the path of the machine we making
+ var/part_path
+
+/obj/item/hfr_box/corner
+ name = "HFR box corner"
+ desc = "Place this as the corner of your 3x3 multiblock fusion reactor"
+ icon_state = "box_corner"
+ box_type = "corner"
+ part_path = /obj/machinery/hypertorus/corner
+
+/obj/item/hfr_box/body
+ name = "HFR box body"
+ desc = "Place this on the sides of the core box of your 3x3 multiblock fusion reactor"
+ box_type = "body"
+ icon_state = "box_body"
+
+/obj/item/hfr_box/body/fuel_input
+ name = "HFR box fuel input"
+ icon_state = "box_fuel"
+ part_path = /obj/machinery/atmospherics/components/unary/hypertorus/fuel_input
+
+/obj/item/hfr_box/body/moderator_input
+ name = "HFR box moderator input"
+ icon_state = "box_moderator"
+ part_path = /obj/machinery/atmospherics/components/unary/hypertorus/moderator_input
+
+/obj/item/hfr_box/body/waste_output
+ name = "HFR box waste output"
+ icon_state = "box_waste"
+ part_path = /obj/machinery/atmospherics/components/unary/hypertorus/waste_output
+
+/obj/item/hfr_box/body/interface
+ name = "HFR box interface"
+ part_path = /obj/machinery/hypertorus/interface
+
+/obj/item/hfr_box/core
+ name = "HFR box core"
+ desc = "Activate this with a multitool to deploy the full machine after setting up the other boxes"
+ icon_state = "box_core"
+ box_type = "core"
+ part_path = /obj/machinery/atmospherics/components/unary/hypertorus/core
+
+/obj/item/hfr_box/core/multitool_act(mob/living/user, obj/item/I)
+ . = ..()
+ var/list/parts = list()
+ for(var/obj/item/hfr_box/box in orange(1,src))
+ var/direction = get_dir(src, box)
+ if(box.box_type == "corner")
+ if(ISDIAGONALDIR(direction))
+ switch(direction)
+ if(NORTHEAST)
+ direction = EAST
+ if(SOUTHEAST)
+ direction = SOUTH
+ if(SOUTHWEST)
+ direction = WEST
+ if(NORTHWEST)
+ direction = NORTH
+ box.dir = direction
+ parts |= box
+ continue
+ if(box.box_type == "body")
+ if(direction in GLOB.cardinals)
+ box.dir = direction
+ parts |= box
+ continue
+ if(parts.len == 8)
+ build_reactor(parts)
+ return
+
+/obj/item/hfr_box/core/proc/build_reactor(list/parts)
+ for(var/obj/item/hfr_box/box in parts)
+ if(box.box_type == "corner")
+ if(!box.part_path || !ispath(box.part_path, /obj/machinery/hypertorus/corner))
+ qdel(box)
+ continue
+ var/obj/machinery/hypertorus/corner/corner = new box.part_path(box.loc)
+ corner.dir = box.dir
+ qdel(box)
+ continue
+ if(box.box_type == "body")
+ if(!box.part_path)
+ qdel(box)
+ continue
+ var/location = get_turf(box)
+ if(box.part_path != /obj/machinery/hypertorus/interface)
+ var/obj/machinery/atmospherics/components/unary/hypertorus/part = new box.part_path(location, TRUE, box.dir)
+ part.dir = box.dir
+ else
+ var/obj/machinery/hypertorus/interface/part = new box.part_path(location)
+ part.dir = box.dir
+ qdel(box)
+ continue
+
+ new/obj/machinery/atmospherics/components/unary/hypertorus/core(loc, TRUE)
+ qdel(src)
diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm
new file mode 100644
index 0000000000000..f467aa98087f2
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/fusion/hfr_procs.dm
@@ -0,0 +1,469 @@
+/**
+ * HFR procs: build, destroy, control. BlueMoon: gas IDs, get_moles/set_moles.
+ */
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_part_connectivity()
+ . = TRUE
+ if(!anchored || panel_open)
+ return FALSE
+ corners.Cut()
+ machine_parts.Cut()
+
+ for(var/obj/machinery/hypertorus/object in orange(1,src))
+ if(. == FALSE)
+ break
+ if(object.panel_open)
+ . = FALSE
+ if(istype(object,/obj/machinery/hypertorus/corner))
+ var/dir = get_dir(src,object)
+ if(dir in GLOB.cardinals)
+ . = FALSE
+ switch(dir)
+ if(SOUTHEAST)
+ if(object.dir != SOUTH)
+ . = FALSE
+ if(SOUTHWEST)
+ if(object.dir != WEST)
+ . = FALSE
+ if(NORTHEAST)
+ if(object.dir != EAST)
+ . = FALSE
+ if(NORTHWEST)
+ if(object.dir != NORTH)
+ . = FALSE
+ corners |= object
+ continue
+ if(get_step(object,REVERSE_DIR(object.dir)) != loc)
+ . = FALSE
+ if(istype(object,/obj/machinery/hypertorus/interface))
+ if(linked_interface && linked_interface != object)
+ . = FALSE
+ linked_interface = object
+
+ for(var/obj/machinery/atmospherics/components/unary/hypertorus/object in orange(1,src))
+ if(. == FALSE)
+ break
+ if(object.panel_open)
+ . = FALSE
+ if(get_step(object,REVERSE_DIR(object.dir)) != loc)
+ . = FALSE
+ if(istype(object,/obj/machinery/atmospherics/components/unary/hypertorus/fuel_input))
+ if(linked_input && linked_input != object)
+ . = FALSE
+ linked_input = object
+ machine_parts |= object
+ if(istype(object,/obj/machinery/atmospherics/components/unary/hypertorus/waste_output))
+ if(linked_output && linked_output != object)
+ . = FALSE
+ linked_output = object
+ machine_parts |= object
+ if(istype(object,/obj/machinery/atmospherics/components/unary/hypertorus/moderator_input))
+ if(linked_moderator && linked_moderator != object)
+ . = FALSE
+ linked_moderator = object
+ machine_parts |= object
+
+ if(!linked_interface || !linked_input || !linked_moderator || !linked_output || corners.len != 4)
+ . = FALSE
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/activate(mob/living/user)
+ if(active)
+ to_chat(user, span_notice("You already activated the machine."))
+ return
+ to_chat(user, span_notice("You link all parts together."))
+ active = TRUE
+ update_appearance(UPDATE_ICON)
+ linked_interface.active = TRUE
+ linked_interface.update_appearance(UPDATE_ICON)
+ RegisterSignal(linked_interface, COMSIG_PARENT_QDELETING, PROC_REF(unregister_signals))
+ linked_input.active = TRUE
+ linked_input.update_appearance(UPDATE_ICON)
+ RegisterSignal(linked_input, COMSIG_PARENT_QDELETING, PROC_REF(unregister_signals))
+ linked_output.active = TRUE
+ linked_output.update_appearance(UPDATE_ICON)
+ RegisterSignal(linked_output, COMSIG_PARENT_QDELETING, PROC_REF(unregister_signals))
+ linked_moderator.active = TRUE
+ linked_moderator.update_appearance(UPDATE_ICON)
+ RegisterSignal(linked_moderator, COMSIG_PARENT_QDELETING, PROC_REF(unregister_signals))
+ for(var/obj/machinery/hypertorus/corner/corner in corners)
+ corner.active = TRUE
+ corner.update_appearance(UPDATE_ICON)
+ RegisterSignal(corner, COMSIG_PARENT_QDELETING, PROC_REF(unregister_signals))
+ soundloop = new(src, TRUE)
+ soundloop.volume = 5
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/unregister_signals(only_signals = FALSE)
+ SIGNAL_HANDLER
+ if(linked_interface)
+ UnregisterSignal(linked_interface, COMSIG_PARENT_QDELETING)
+ if(linked_input)
+ UnregisterSignal(linked_input, COMSIG_PARENT_QDELETING)
+ if(linked_output)
+ UnregisterSignal(linked_output, COMSIG_PARENT_QDELETING)
+ if(linked_moderator)
+ UnregisterSignal(linked_moderator, COMSIG_PARENT_QDELETING)
+ for(var/obj/machinery/hypertorus/corner/corner in corners)
+ UnregisterSignal(corner, COMSIG_PARENT_QDELETING)
+ if(!only_signals)
+ deactivate()
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/deactivate()
+ if(!active)
+ return
+ active = FALSE
+ update_appearance(UPDATE_ICON)
+ if(linked_interface)
+ linked_interface.active = FALSE
+ linked_interface.update_appearance(UPDATE_ICON)
+ linked_interface = null
+ if(linked_input)
+ linked_input.active = FALSE
+ linked_input.update_appearance(UPDATE_ICON)
+ linked_input = null
+ if(linked_output)
+ linked_output.active = FALSE
+ linked_output.update_appearance(UPDATE_ICON)
+ linked_output = null
+ if(linked_moderator)
+ linked_moderator.active = FALSE
+ linked_moderator.update_appearance(UPDATE_ICON)
+ linked_moderator = null
+ if(corners.len)
+ for(var/obj/machinery/hypertorus/corner/corner in corners)
+ corner.active = FALSE
+ corner.update_appearance(UPDATE_ICON)
+ corners = list()
+ QDEL_NULL(soundloop)
+
+/// Removed: was iterating GLOB.gas_data.ids every tick and doing set_moles(get_moles(...)) — no-op and major perf sink. get_moles() already returns 0 for missing gases.
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/assert_gases()
+ return
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/update_pipenets()
+ update_parents()
+ linked_input.update_parents()
+ linked_output.update_parents()
+ linked_moderator.update_parents()
+
+/// Обновляет архивные темпы и выставляет power_level по температуре fusion (диапазоны 500, 1e3, 1e4, ... до 6).
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/update_temperature_status(seconds_per_tick)
+ fusion_temperature_archived = fusion_temperature
+ fusion_temperature = internal_fusion.return_temperature()
+ moderator_temperature_archived = moderator_temperature
+ moderator_temperature = moderator_internal.return_temperature()
+ coolant_temperature_archived = coolant_temperature
+ coolant_temperature = airs[1].return_temperature()
+ output_temperature_archived = output_temperature
+ output_temperature = linked_output.airs[1].return_temperature()
+ temperature_period = seconds_per_tick
+ switch(fusion_temperature)
+ if(-INFINITY to 500)
+ power_level = 0
+ if(500 to 1e3)
+ power_level = 1
+ if(1e3 to 1e4)
+ power_level = 2
+ if(1e4 to 1e5)
+ power_level = 3
+ if(1e5 to 1e6)
+ power_level = 4
+ if(1e6 to 1e7)
+ power_level = 5
+ else
+ power_level = 6
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/play_ambience(seconds_per_tick)
+ if(last_accent_sound < world.time && SPT_PROB(10, seconds_per_tick))
+ var/aggression = min(((critical_threshold_proximity / 800) * ((power_level) / 5)), 1.0) * 100
+ if(critical_threshold_proximity >= 300)
+ playsound(src, SFX_HYPERTORUS_MELTING, max(50, aggression), FALSE, 40, 30, falloff_distance = 10)
+ else
+ playsound(src, SFX_HYPERTORUS_CALM, max(50, aggression), FALSE, 25, 25, falloff_distance = 10)
+ var/next_sound = round((100 - aggression) * 5) + 5
+ last_accent_sound = world.time + max(HYPERTORUS_ACCENT_SOUND_MIN_COOLDOWN, next_sound)
+ var/ambient_hum = 1
+ if(check_fuel())
+ ambient_hum = power_level + 1
+ soundloop.volume = clamp(ambient_hum * 8, 0, 50)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_fuel()
+ if(!selected_fuel)
+ return FALSE
+ if(!internal_fusion.total_moles())
+ return FALSE
+ for(var/gas_id in selected_fuel.requirements)
+ if(internal_fusion.get_moles(gas_id) < HFR_FUSION_MOLE_THRESHOLD)
+ return FALSE
+ return TRUE
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_power_use()
+ if(machine_stat & (NOPOWER|BROKEN))
+ return FALSE
+ if(use_power == ACTIVE_POWER_USE)
+ use_power = ACTIVE_POWER_USE
+ active_power_usage = (power_level + 1) * MIN_POWER_USAGE
+ return TRUE
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_gas_requirements()
+ var/datum/gas_mixture/contents = linked_input.airs[1]
+ for(var/gas_id in selected_fuel.requirements)
+ if(contents.get_moles(gas_id) <= 0)
+ return FALSE
+ return TRUE
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/dump_gases()
+ var/datum/gas_mixture/remove = internal_fusion.remove(internal_fusion.total_moles())
+ if(remove)
+ linked_output.airs[1].merge(remove)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/get_status()
+ var/integrity = get_integrity_percent()
+ if(integrity < HYPERTORUS_MELTING_PERCENT)
+ return HYPERTORUS_MELTING
+ if(integrity < HYPERTORUS_EMERGENCY_PERCENT)
+ return HYPERTORUS_EMERGENCY
+ if(integrity < HYPERTORUS_DANGER_PERCENT)
+ return HYPERTORUS_DANGER
+ if(integrity < HYPERTORUS_WARNING_PERCENT)
+ return HYPERTORUS_WARNING
+ if(power_level > 0)
+ return HYPERTORUS_NOMINAL
+ return HYPERTORUS_INACTIVE
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/alarm()
+ switch(get_status())
+ if(HYPERTORUS_MELTING)
+ playsound(src, 'sound/misc/bloblarm.ogg', 100, FALSE, 40, 30, falloff_distance = 10)
+ if(HYPERTORUS_EMERGENCY)
+ playsound(src, 'sound/machines/engine_alert1.ogg', 100, FALSE, 30, 30, falloff_distance = 10)
+ if(HYPERTORUS_DANGER)
+ playsound(src, 'sound/machines/engine_alert2.ogg', 100, FALSE, 30, 30, falloff_distance = 10)
+ if(HYPERTORUS_WARNING)
+ playsound(src, 'sound/machines/terminal_alert.ogg', 75)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/get_integrity_percent()
+ var/integrity = critical_threshold_proximity / melting_point
+ integrity = round(100 - integrity * 100, 0.01)
+ integrity = integrity < 0 ? 0 : integrity
+ return integrity
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/get_area_cell_percent()
+ var/area/area = get_area(src)
+ if(!area)
+ return 0
+ var/obj/machinery/power/apc/apc = area.get_apc()
+ if(!apc)
+ return 0
+ var/obj/item/stock_parts/cell/cell = apc.cell
+ if(!cell)
+ return 0
+ return cell.percent()
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_alert()
+ if(critical_threshold_proximity < warning_point)
+ return
+ if((REALTIMEOFDAY - lastwarning) / 10 >= WARNING_TIME_DELAY)
+ alarm()
+ if(critical_threshold_proximity > emergency_point)
+ radio.talk_into(src, "[emergency_alert] Integrity: [get_integrity_percent()]%", common_channel)
+ lastwarning = REALTIMEOFDAY
+ if(!has_reached_emergency)
+ investigate_log("has reached the emergency point for the first time.", INVESTIGATE_HYPERTORUS)
+ message_admins("[src] has reached the emergency point [ADMIN_JMP(src)].")
+ has_reached_emergency = TRUE
+ send_radio_explanation()
+ else if(critical_threshold_proximity >= critical_threshold_proximity_archived)
+ radio.talk_into(src, "[warning_alert] Integrity: [get_integrity_percent()]%", engineering_channel)
+ lastwarning = REALTIMEOFDAY - (WARNING_TIME_DELAY * 5)
+ send_radio_explanation()
+ else
+ radio.talk_into(src, "[safe_alert] Integrity: [get_integrity_percent()]%", engineering_channel)
+ lastwarning = REALTIMEOFDAY
+ if(critical_threshold_proximity >= melting_point)
+ countdown()
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/emp_act(severity)
+ . = ..()
+ if(. & EMP_PROTECT_SELF)
+ return
+ warning_damage_flags |= HYPERTORUS_FLAG_EMPED
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/send_radio_explanation()
+ if(warning_damage_flags & HYPERTORUS_FLAG_EMPED)
+ var/list/characters = list()
+ characters += GLOB.alphabet
+ for(var/c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
+ characters += c
+ for(var/c in "0123456789")
+ characters += c
+ characters += " "
+ characters += " "
+ var/message = random_string(rand(50,70), characters)
+ radio.talk_into(src, "[message]", engineering_channel)
+ return
+ if(warning_damage_flags & HYPERTORUS_FLAG_HIGH_POWER_DAMAGE)
+ radio.talk_into(src, "Warning! Shield destabilizing due to excessive power!", engineering_channel)
+ if(warning_damage_flags & HYPERTORUS_FLAG_IRON_CONTENT_DAMAGE)
+ radio.talk_into(src, "Warning! Iron shards are damaging the internal core shielding!", engineering_channel)
+ if(warning_damage_flags & HYPERTORUS_FLAG_HIGH_FUEL_MIX_MOLE)
+ radio.talk_into(src, "Warning! Fuel mix moles reaching critical levels!", engineering_channel)
+ if(warning_damage_flags & HYPERTORUS_FLAG_IRON_CONTENT_INCREASE)
+ radio.talk_into(src, "Warning! Iron amount inside the core is increasing!", engineering_channel)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/countdown()
+ set waitfor = FALSE
+ if(final_countdown)
+ return
+ final_countdown = TRUE
+ var/critical = selected_fuel.meltdown_flags & HYPERTORUS_FLAG_CRITICAL_MELTDOWN
+ if(critical)
+ priority_announce("ВНИМАНИЕ! Взрыв ХФР, скорее всего, охватит большую часть станции, а грядущий ЭМИ уничтожит большую часть электроники. \
+ Отойдите как можно дальше от реактора или найдите способ его остановить от расщепления.", "ВНИМАНИЕ", 'sound/announcer/notice/notice3.ogg')
+ var/speaking = "[emergency_alert] The Hypertorus fusion reactor has reached critical integrity failure. Emergency magnetic dampeners online."
+ radio.talk_into(src, speaking, common_channel)
+ notify_ghosts("The [src] has begun melting down!", 'sound/machines/warning-buzzer.ogg', FALSE, src, header = "Meltdown Incoming")
+ for(var/i in HYPERTORUS_COUNTDOWN_TIME to 0 step -10)
+ if(QDELETED(src))
+ return
+ if(critical_threshold_proximity < melting_point)
+ radio.talk_into(src, "[safe_alert] Failsafe has been disengaged.", common_channel)
+ final_countdown = FALSE
+ return
+ else if((i % 50) != 0 && i > 50)
+ sleep(1 SECONDS)
+ continue
+ else if(i > 50)
+ if(i == 10 SECONDS && critical)
+ sound_to_playing_players('sound/machines/warning-buzzer.ogg')
+ speaking = "[DisplayTimeText(i, TRUE)] remain before total integrity failure."
+ else
+ speaking = "[i*0.1]..."
+ radio.talk_into(src, speaking, common_channel)
+ sleep(1 SECONDS)
+ meltdown()
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/meltdown()
+ var/flash_explosion = 0
+ var/light_impact_explosion = 0
+ var/heavy_impact_explosion = 0
+ var/devastating_explosion = 0
+ var/em_pulse = selected_fuel.meltdown_flags & HYPERTORUS_FLAG_EMP
+ var/rad_pulse = selected_fuel.meltdown_flags & HYPERTORUS_FLAG_RADIATION_PULSE
+ var/emp_heavy_size = 0
+ var/rad_pulse_size = 0
+ var/gas_spread = 0
+ var/gas_pockets = 0
+ var/critical = selected_fuel.meltdown_flags & HYPERTORUS_FLAG_CRITICAL_MELTDOWN
+ if(selected_fuel.meltdown_flags & HYPERTORUS_FLAG_BASE_EXPLOSION)
+ flash_explosion = power_level * 3
+ light_impact_explosion = power_level * 2
+ if(selected_fuel.meltdown_flags & HYPERTORUS_FLAG_MEDIUM_EXPLOSION)
+ flash_explosion = power_level * 6
+ light_impact_explosion = power_level * 5
+ heavy_impact_explosion = power_level * 0.5
+ if(selected_fuel.meltdown_flags & HYPERTORUS_FLAG_DEVASTATING_EXPLOSION)
+ flash_explosion = power_level * 8
+ light_impact_explosion = power_level * 7
+ heavy_impact_explosion = power_level * 2
+ devastating_explosion = power_level
+ if(selected_fuel.meltdown_flags & HYPERTORUS_FLAG_MINIMUM_SPREAD)
+ if(em_pulse)
+ emp_heavy_size = power_level * 1
+ if(rad_pulse)
+ rad_pulse_size = 2 * power_level + 8
+ gas_pockets = 5
+ gas_spread = power_level * 2
+ if(selected_fuel.meltdown_flags & HYPERTORUS_FLAG_MEDIUM_SPREAD)
+ if(em_pulse)
+ emp_heavy_size = power_level * 3
+ if(rad_pulse)
+ rad_pulse_size = power_level + 24
+ gas_pockets = 7
+ gas_spread = power_level * 4
+ if(selected_fuel.meltdown_flags & HYPERTORUS_FLAG_BIG_SPREAD)
+ if(em_pulse)
+ emp_heavy_size = power_level * 5
+ if(rad_pulse)
+ rad_pulse_size = power_level + 34
+ gas_pockets = 10
+ gas_spread = power_level * 6
+ if(selected_fuel.meltdown_flags & HYPERTORUS_FLAG_MASSIVE_SPREAD)
+ if(em_pulse)
+ emp_heavy_size = power_level * 7
+ if(rad_pulse)
+ rad_pulse_size = power_level + 44
+ gas_pockets = 15
+ gas_spread = power_level * 8
+ var/list/around_turfs = spiral_range_turfs(gas_spread, src)
+ var/list/turfs_to_remove = list()
+ for(var/turf/turf as anything in around_turfs)
+ if(isclosedturf(turf) || isspaceturf(turf))
+ turfs_to_remove += turf
+ around_turfs -= turfs_to_remove
+ var/datum/gas_mixture/remove_fusion
+ if(internal_fusion.total_moles() > 0)
+ remove_fusion = internal_fusion.remove_ratio(0.2)
+ var/datum/gas_mixture/remove
+ for(var/i in 1 to gas_pockets)
+ remove = remove_fusion.remove_ratio(1/gas_pockets)
+ var/turf/local = pick(around_turfs)
+ local.assume_air(remove)
+ loc.assume_air(internal_fusion)
+ var/datum/gas_mixture/remove_moderator
+ if(moderator_internal.total_moles() > 0)
+ remove_moderator = moderator_internal.remove_ratio(0.2)
+ var/datum/gas_mixture/remove
+ for(var/i in 1 to gas_pockets)
+ remove = remove_moderator.remove_ratio(1/gas_pockets)
+ var/turf/local = pick(around_turfs)
+ local.assume_air(remove)
+ loc.assume_air(moderator_internal)
+ explosion(loc, critical ? devastating_explosion * 2 : devastating_explosion, critical ? heavy_impact_explosion * 2 : heavy_impact_explosion, light_impact_explosion, flash_explosion, TRUE, TRUE)
+ if(rad_pulse)
+ radiation_pulse(src, 3000, rad_pulse_size, TRUE)
+ if(em_pulse)
+ empulse_using_range(loc, critical ? emp_heavy_size * 2 : emp_heavy_size, TRUE)
+ qdel(src)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_cracked_parts()
+ for(var/obj/machinery/atmospherics/components/unary/hypertorus/part in machine_parts)
+ if(part.cracked)
+ return part
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/create_crack() as /obj/machinery/atmospherics/components/unary/hypertorus
+ var/obj/machinery/atmospherics/components/unary/hypertorus/part = pick(machine_parts)
+ part.cracked = TRUE
+ part.update_appearance(UPDATE_ICON)
+ return part
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/spill_gases(obj/origin, datum/gas_mixture/target_mix, ratio)
+ var/datum/gas_mixture/remove_mixture = target_mix.remove_ratio(ratio)
+ var/turf/origin_turf = origin.loc
+ if(!origin_turf)
+ return
+ origin_turf.assume_air(remove_mixture)
+
+/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/process_moderator_overflow(seconds_per_tick)
+ var/obj/machinery/atmospherics/components/unary/hypertorus/cracked_part = check_cracked_parts()
+ if(cracked_part)
+ var/leak_rate
+ if(moderator_internal.return_pressure() < HYPERTORUS_MEDIUM_SPILL_PRESSURE)
+ if(!prob(HYPERTORUS_WEAK_SPILL_CHANCE))
+ return
+ leak_rate = HYPERTORUS_WEAK_SPILL_RATE
+ else if(moderator_internal.return_pressure() < HYPERTORUS_STRONG_SPILL_PRESSURE)
+ leak_rate = HYPERTORUS_MEDIUM_SPILL_RATE
+ else
+ leak_rate = HYPERTORUS_STRONG_SPILL_RATE
+ spill_gases(cracked_part, moderator_internal, ratio = 1 - (1 - leak_rate) ** seconds_per_tick)
+ return
+ if(moderator_internal.total_moles() < HYPERTORUS_HYPERCRITICAL_MOLES)
+ return
+ cracked_part = create_crack()
+ if(moderator_internal.return_pressure() < HYPERTORUS_MEDIUM_SPILL_PRESSURE)
+ return
+ if(moderator_internal.return_pressure() < HYPERTORUS_STRONG_SPILL_PRESSURE)
+ explosion(get_turf(cracked_part), 0, 0, 1, 3, TRUE, FALSE, 3)
+ spill_gases(cracked_part, moderator_internal, ratio = HYPERTORUS_MEDIUM_SPILL_INITIAL)
+ return
+ explosion(get_turf(cracked_part), 0, 1, 3, 5, TRUE, FALSE, 5)
+ spill_gases(cracked_part, moderator_internal, ratio = HYPERTORUS_STRONG_SPILL_INITIAL)
diff --git a/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm
new file mode 100644
index 0000000000000..696739b550948
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm
@@ -0,0 +1,318 @@
+#define MIN_PROGRESS_AMOUNT 3
+#define MIN_DEVIATION_RATE 0.90
+#define MAX_DEVIATION_RATE 1.1
+#define HIGH_CONDUCTIVITY_RATIO 0.95
+
+/obj/machinery/atmospherics/components/binary/crystallizer
+ icon = 'icons/obj/machines/atmospherics/machines.dmi'
+ icon_state = "crystallizer-off"
+ base_icon_state = "crystallizer"
+ name = "crystallizer"
+ desc = "Used to crystallize or solidify gases."
+ layer = ABOVE_MOB_LAYER
+ density = TRUE
+ max_integrity = 300
+ armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 100, BOMB = 0, BIO = 100, RAD = 100, FIRE = 80, ACID = 30)
+ circuit = /obj/item/circuitboard/machine/crystallizer
+ pipe_flags = PIPING_ONE_PER_TURF | PIPING_DEFAULT_LAYER_ONLY
+
+ var/datum/gas_mixture/internal
+ var/gas_input = 0
+ var/progress_bar = 0
+ var/quality_loss = 0
+ var/datum/gas_recipe/selected_recipe = null
+ var/total_recipe_moles = 0
+
+/obj/machinery/atmospherics/components/binary/crystallizer/Initialize(mapload)
+ . = ..(mapload)
+ internal = new
+ register_context()
+
+/obj/machinery/atmospherics/components/binary/crystallizer/on_deconstruction(disassembled)
+ var/turf/local_turf = get_turf(loc)
+ if(internal.total_moles() > 0)
+ local_turf.assume_air(internal)
+ return ..()
+
+/obj/machinery/atmospherics/components/binary/crystallizer/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ context[SCREENTIP_CONTEXT_CTRL_LMB] = "Turn [on ? "off" : "on"]"
+ if(!held_item)
+ return CONTEXTUAL_SCREENTIP_SET
+ switch(held_item.tool_behaviour)
+ if(TOOL_SCREWDRIVER)
+ context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel"
+ if(TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_RMB] = "Rotate"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/machinery/atmospherics/components/binary/crystallizer/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers)
+ if(!on)
+ if(default_deconstruction_screwdriver(user, "[base_icon_state]-open", "[base_icon_state]-off", I))
+ return
+ if(default_change_direction_wrench(user, I))
+ return
+ return ..()
+
+/obj/machinery/atmospherics/components/binary/crystallizer/crowbar_act(mob/living/user, obj/item/tool)
+ if(internal.return_pressure() > 0)
+ say("WARNING - Internal pressure present, deconstruct with caution!")
+ return default_deconstruction_crowbar(tool)
+
+/obj/machinery/atmospherics/components/binary/crystallizer/update_overlays()
+ . = ..()
+ var/mutable_appearance/pipe_appearance1 = mutable_appearance('icons/obj/pipes_n_cables/pipe_underlays.dmi', "intact_[dir]_[piping_layer]", layer = GAS_SCRUBBER_LAYER)
+ pipe_appearance1.color = COLOR_LIME
+ var/mutable_appearance/pipe_appearance2 = mutable_appearance('icons/obj/pipes_n_cables/pipe_underlays.dmi', "intact_[REVERSE_DIR(dir)]_[piping_layer]", layer = GAS_SCRUBBER_LAYER)
+ pipe_appearance2.color = COLOR_MOSTLY_PURE_RED
+ . += pipe_appearance1
+ . += pipe_appearance2
+
+/obj/machinery/atmospherics/components/binary/crystallizer/update_icon_state()
+ . = ..()
+ if(panel_open)
+ icon_state = "[base_icon_state]-open"
+ else if(on && is_operational)
+ icon_state = "[base_icon_state]-on"
+ else
+ icon_state = "[base_icon_state]-off"
+
+/obj/machinery/atmospherics/components/binary/crystallizer/CtrlClick(mob/user)
+ if(!can_interact(user))
+ return
+ if(!is_operational)
+ return
+ if(panel_open)
+ balloon_alert(user, "close panel!")
+ return
+ on = !on
+ balloon_alert(user, "turned [on ? "on" : "off"]")
+ investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
+ update_icon()
+
+/obj/machinery/atmospherics/components/binary/crystallizer/proc/check_temp_requirements()
+ if(internal.return_temperature() >= selected_recipe.min_temp * MIN_DEVIATION_RATE && internal.return_temperature() <= selected_recipe.max_temp * MAX_DEVIATION_RATE)
+ return TRUE
+ return FALSE
+
+/obj/machinery/atmospherics/components/binary/crystallizer/proc/inject_gases()
+ var/datum/gas_mixture/contents = airs[2]
+ for(var/gas_id in selected_recipe.requirements)
+ var/moles_in = contents.get_moles(gas_id)
+ if(moles_in <= 0)
+ continue
+ var/moles_internal = internal.get_moles(gas_id)
+ if(moles_internal >= selected_recipe.requirements[gas_id] * 2)
+ continue
+ var/amount = moles_in * gas_input
+ var/datum/gas_mixture/removed = contents.remove_specific(gas_id, amount)
+ if(removed)
+ internal.merge(removed)
+
+/obj/machinery/atmospherics/components/binary/crystallizer/proc/internal_check()
+ var/gas_check = 0
+ for(var/gas_id in selected_recipe.requirements)
+ var/moles = internal.get_moles(gas_id)
+ if(moles <= 0)
+ return FALSE
+ if(moles >= selected_recipe.requirements[gas_id])
+ gas_check++
+ if(gas_check == selected_recipe.requirements.len)
+ return TRUE
+ return FALSE
+
+/// Качество: у краёв диапазона темпов (min/max рецепта) quality_loss растёт; у медианы падает. Потом внутренняя темп сдвигается на energy_release с учётом теплоёмкости.
+/obj/machinery/atmospherics/components/binary/crystallizer/proc/heat_calculations()
+ var/quality_rate = MIN_PROGRESS_AMOUNT * 0.5 * clamp(total_recipe_moles / 20, 0.1, 5)
+ var/internal_temp = internal.return_temperature()
+ if((internal_temp >= (selected_recipe.min_temp * MIN_DEVIATION_RATE) && internal_temp <= selected_recipe.min_temp) || \
+ (internal_temp >= selected_recipe.max_temp && internal_temp <= (selected_recipe.max_temp * MAX_DEVIATION_RATE)))
+ quality_loss = min(quality_loss + quality_rate, 100)
+
+ var/median_temperature = (selected_recipe.max_temp + selected_recipe.min_temp) / 2
+ if(internal_temp >= (median_temperature * MIN_DEVIATION_RATE) && internal_temp <= (median_temperature * MAX_DEVIATION_RATE))
+ quality_loss = max(quality_loss - quality_rate, -85)
+
+ var/heat_cap = max(internal.heat_capacity(), 1e-10)
+ internal.set_temperature(max(internal_temp + (selected_recipe.energy_release / heat_cap), TCMB))
+
+/// Теплообмен между портом охлаждения (airs[1]) и внутренней смесью. Количество тепла по разности температур и теплоёмкостям, коэффициент HIGH_CONDUCTIVITY_RATIO.
+/obj/machinery/atmospherics/components/binary/crystallizer/proc/heat_conduction()
+ var/datum/gas_mixture/cooling_port = airs[1]
+ if(cooling_port.total_moles() > MINIMUM_MOLE_COUNT)
+ if(internal.total_moles() > 0)
+ var/cooling_heat_capacity = cooling_port.heat_capacity()
+ var/internal_heat_capacity = internal.heat_capacity()
+ if(cooling_heat_capacity <= 0 || internal_heat_capacity <= 0)
+ return
+ var/coolant_temperature_delta = cooling_port.return_temperature() - internal.return_temperature()
+ var/cooling_heat_amount = HIGH_CONDUCTIVITY_RATIO * coolant_temperature_delta * (cooling_heat_capacity * internal_heat_capacity / (cooling_heat_capacity + internal_heat_capacity))
+ cooling_port.set_temperature(max(cooling_port.return_temperature() - cooling_heat_amount / cooling_heat_capacity, TCMB))
+ internal.set_temperature(max(internal.return_temperature() + cooling_heat_amount / internal_heat_capacity, TCMB))
+
+/obj/machinery/atmospherics/components/binary/crystallizer/proc/moles_calculations()
+ var/amounts = 0
+ for(var/gas_id in selected_recipe.requirements)
+ amounts += selected_recipe.requirements[gas_id]
+ total_recipe_moles = amounts
+
+/obj/machinery/atmospherics/components/binary/crystallizer/proc/dump_gases()
+ var/datum/gas_mixture/remove = internal.remove(internal.total_moles())
+ airs[2].merge(remove)
+ internal.clear()
+
+/// За тик: подкачка газов из входа, теплообмен, при выполнении требований рецепта и темпы считаем качество и прогресс. При progress_bar == 100 потребляем газы и выдаём предметы.
+/obj/machinery/atmospherics/components/binary/crystallizer/process_atmos()
+ if(!on || !is_operational || selected_recipe == null)
+ return
+
+ inject_gases()
+
+ if(!internal.total_moles())
+ return
+
+ heat_conduction()
+
+ if(internal_check())
+ if(check_temp_requirements())
+ heat_calculations()
+ var/progress_step = MIN_PROGRESS_AMOUNT * 0.5 * clamp(total_recipe_moles / 20, 0.5, 2)
+ progress_bar = min(progress_bar + progress_step, 100)
+ else
+ quality_loss = min(quality_loss + 0.5, 100)
+ progress_bar = max(progress_bar - 1, 0)
+ if(progress_bar != 100)
+ update_parents()
+ return
+ progress_bar = 0
+
+ for(var/gas_id in selected_recipe.requirements)
+ var/required_gas_moles = selected_recipe.requirements[gas_id]
+ var/amount_consumed = required_gas_moles + (required_gas_moles * (quality_loss * 0.01))
+ if(internal.get_moles(gas_id) < amount_consumed)
+ quality_loss = min(quality_loss + 10, 100)
+ internal.remove_specific(gas_id, amount_consumed)
+
+ var/total_quality = clamp(50 - quality_loss, 0, 100)
+ var/quality_control
+ switch(total_quality)
+ if(100)
+ quality_control = "Masterwork"
+ if(95 to 99)
+ quality_control = "Supreme"
+ if(75 to 94)
+ quality_control = "Good"
+ if(65 to 74)
+ quality_control = "Decent"
+ if(55 to 64)
+ quality_control = "Average"
+ if(35 to 54)
+ quality_control = "Ok"
+ if(15 to 34)
+ quality_control = "Poor"
+ if(5 to 14)
+ quality_control = "Ugly"
+ if(1 to 4)
+ quality_control = "Cracked"
+ if(0)
+ quality_control = "Oh God why"
+
+ for(var/path in selected_recipe.products)
+ var/amount_produced = selected_recipe.products[path]
+ for(var/i in 1 to amount_produced)
+ var/obj/creation = new path(get_step(src, SOUTH))
+ creation.name = "[quality_control] [creation.name]"
+ if(selected_recipe.dangerous)
+ investigate_log("[creation.name] has been created in the crystallizer.", INVESTIGATE_ATMOS)
+ message_admins("[creation.name] has been created in the crystallizer [ADMIN_JMP(src)].")
+
+ quality_loss = 0
+ update_parents()
+
+/obj/machinery/atmospherics/components/binary/crystallizer/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "Crystallizer", name)
+ ui.open()
+
+/obj/machinery/atmospherics/components/binary/crystallizer/ui_static_data()
+ var/data = list()
+ data["selected_recipes"] = list(list("name" = "Nothing", "id" = ""))
+ for(var/id in GLOB.gas_recipe_meta)
+ var/datum/gas_recipe/recipe = GLOB.gas_recipe_meta[id]
+ if(recipe.machine_type != "Crystallizer")
+ continue
+ data["selected_recipes"] += list(list("name" = recipe.name, "id" = recipe.id))
+ return data
+
+/obj/machinery/atmospherics/components/binary/crystallizer/ui_data()
+ var/data = list()
+ data["on"] = on
+
+ if(selected_recipe)
+ data["selected"] = selected_recipe.id
+ else
+ data["selected"] = ""
+
+ var/list/internal_gas_data = list()
+ for(var/gas_id in internal.get_gases())
+ internal_gas_data.Add(list(list(
+ "name" = GLOB.gas_data.names[gas_id],
+ "id" = gas_id,
+ "amount" = round(internal.get_moles(gas_id), 0.01),
+ )))
+ data["internal_gas_data"] = internal_gas_data
+
+ var/list/requirements
+ if(!selected_recipe)
+ requirements = list("Select a recipe to see the requirements")
+ else
+ requirements = list("To create [selected_recipe.name] you will need:")
+ for(var/gas_id in selected_recipe.requirements)
+ var/amount_consumed = selected_recipe.requirements[gas_id]
+ requirements += "-[amount_consumed] moles of [GLOB.gas_data.names[gas_id]]"
+ requirements += "In a temperature range between [selected_recipe.min_temp] K and [selected_recipe.max_temp] K"
+ requirements += "The crystallization reaction will be [selected_recipe.energy_release ? (selected_recipe.energy_release > 0 ? "exothermic" : "endothermic") : "thermally neutral"]"
+ data["requirements"] = requirements.Join("\n")
+
+ data["internal_temperature"] = internal.total_moles() ? internal.return_temperature() : 0
+ data["progress_bar"] = progress_bar
+ data["gas_input"] = gas_input
+ return data
+
+/obj/machinery/atmospherics/components/binary/crystallizer/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("power")
+ on = !on
+ investigate_log("was turned [on ? "on" : "off"] by [key_name(usr)]", INVESTIGATE_ATMOS)
+ . = TRUE
+ if("recipe")
+ selected_recipe = null
+ var/recipe_name = "nothing"
+ var/datum/gas_recipe/recipe = GLOB.gas_recipe_meta[params["mode"]]
+ if(internal.total_moles() > 0)
+ dump_gases()
+ quality_loss = 0
+ progress_bar = 0
+ if(recipe && recipe.id != "")
+ selected_recipe = recipe
+ recipe_name = recipe.name
+ update_parents()
+ moles_calculations()
+ investigate_log("was set to recipe [recipe_name ? recipe_name : "null"] by [key_name(usr)]", INVESTIGATE_ATMOS)
+ . = TRUE
+ if("gas_input")
+ var/_gas_input = params["gas_input"]
+ gas_input = clamp(_gas_input, 0, 250)
+ update_icon()
+
+/obj/machinery/atmospherics/components/binary/crystallizer/update_layer()
+ return
+
+#undef MIN_PROGRESS_AMOUNT
+#undef MIN_DEVIATION_RATE
+#undef MAX_DEVIATION_RATE
+#undef HIGH_CONDUCTIVITY_RATIO
diff --git a/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_extra_items.dm b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_extra_items.dm
new file mode 100644
index 0000000000000..bb96235e4b02b
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_extra_items.dm
@@ -0,0 +1,239 @@
+/// Crystallizer recipes: gas crystal grenades, fuel pellets, stacks (from WhiteMoon)
+/// Sprites: copy from WhiteMoon icons/obj/weapons/grenade.dmi, exploration.dmi, stack_objects.dmi, mineral sheets
+
+// === Gas crystal grenades (base + types) ===
+/obj/item/grenade/gas_crystal
+ desc = "A crystal from the crystallizer."
+ name = "Gas Crystal"
+ icon = 'icons/obj/crystallizer_grenades.dmi'
+ icon_state = "bluefrag"
+ item_state = "flashbang"
+ resistance_flags = FIRE_PROOF
+
+/obj/item/grenade/gas_crystal/prime(mob/living/lanced_by)
+ ..()
+ update_mob()
+
+/obj/item/grenade/gas_crystal/healium_crystal
+ name = "Healium crystal"
+ desc = "A crystal made from Healium gas, cold to the touch."
+ icon_state = "healium_crystal"
+ var/fix_range = 7
+ /// Healium moles released per turf (scaled by distance)
+ var/healium_per_turf = 50
+
+/obj/item/grenade/gas_crystal/healium_crystal/prime(mob/living/lanced_by)
+ ..()
+ playsound(src, 'sound/effects/spray2.ogg', 100, TRUE)
+ for(var/turf/open/T in range(fix_range, src))
+ var/dist = max(get_dist(T, src), 1)
+ T.air.adjust_moles(GAS_HEALIUM, healium_per_turf / dist)
+ T.air.adjust_moles(GAS_O2, MOLES_O2STANDARD * 0.5 / dist)
+ T.air.adjust_moles(GAS_N2, MOLES_N2STANDARD * 0.5 / dist)
+ T.air.set_temperature(T20C)
+ qdel(src)
+
+/obj/item/grenade/gas_crystal/proto_nitrate_crystal
+ name = "Proto Nitrate crystal"
+ desc = "A crystal made from Proto Nitrate gas."
+ icon_state = "proto_nitrate_crystal"
+ var/refill_range = 5
+ var/n2_gas_amount = 80
+ var/o2_gas_amount = 30
+ /// Proto nitrate moles released per turf (scaled by distance)
+ var/proto_nitrate_amount = 40
+
+/obj/item/grenade/gas_crystal/proto_nitrate_crystal/prime(mob/living/lanced_by)
+ ..()
+ playsound(src, 'sound/effects/spray2.ogg', 100, TRUE)
+ for(var/turf/open/T in view(refill_range, src))
+ var/dist = max(get_dist(T, src), 1)
+ T.air.adjust_moles(GAS_PROTO_NITRATE, proto_nitrate_amount / dist)
+ T.air.adjust_moles(GAS_N2, n2_gas_amount / dist)
+ T.air.adjust_moles(GAS_O2, o2_gas_amount / dist)
+ qdel(src)
+
+/obj/item/grenade/gas_crystal/nitrous_oxide_crystal
+ name = "N2O crystal"
+ desc = "A crystal made from N2O gas."
+ icon_state = "n2o_crystal"
+ var/fill_range = 3
+ var/n2o_gas_amount = 50
+
+/obj/item/grenade/gas_crystal/nitrous_oxide_crystal/prime(mob/living/lanced_by)
+ ..()
+ playsound(src, 'sound/effects/spray2.ogg', 100, TRUE)
+ for(var/turf/open/T in range(fill_range, src))
+ var/dist = max(get_dist(T, src), 1)
+ T.air.adjust_moles(GAS_NITROUS, n2o_gas_amount / dist)
+ qdel(src)
+
+/obj/item/grenade/gas_crystal/crystal_foam
+ name = "crystal foam"
+ desc = "A crystal with a foggy inside."
+ icon_state = "crystal_foam"
+ var/breach_range = 7
+
+/obj/item/grenade/gas_crystal/crystal_foam/prime(mob/living/lanced_by)
+ ..()
+ var/datum/reagents/first_batch = new(75)
+ var/datum/reagents/second_batch = new(50)
+ first_batch.add_reagent(/datum/reagent/aluminium, 75)
+ second_batch.add_reagent(/datum/reagent/smart_foaming_agent, 25)
+ second_batch.add_reagent(/datum/reagent/toxin/acid/fluacid, 25)
+ chem_splash(get_turf(src), breach_range, list(first_batch, second_batch))
+ playsound(src, 'sound/effects/spray2.ogg', 100, TRUE)
+ update_mob()
+ qdel(src)
+
+// === Fuel pellets ===
+/obj/item/fuel_pellet
+ name = "standard fuel pellet"
+ desc = "A compressed fuel pellet."
+ icon = 'icons/obj/crystallizer_exploration.dmi'
+ icon_state = "fuel_basic"
+ w_class = WEIGHT_CLASS_SMALL
+ var/uses = 5
+
+/obj/item/fuel_pellet/advanced
+ name = "advanced fuel pellet"
+ icon_state = "fuel_advanced"
+
+/obj/item/fuel_pellet/exotic
+ name = "exotic fuel pellet"
+ icon_state = "fuel_exotic"
+
+// === Stacks (crystallizer products) ===
+/obj/item/stack/ammonia_crystals
+ name = "ammonia crystals"
+ singular_name = "ammonia crystal"
+ icon = 'icons/obj/crystallizer_sheets.dmi'
+ icon_state = "ammonia_crystal"
+ w_class = WEIGHT_CLASS_TINY
+ resistance_flags = FLAMMABLE
+ max_amount = 50
+ grind_results = list(/datum/reagent/ammonia = 10)
+ merge_type = /obj/item/stack/ammonia_crystals
+
+/obj/item/stack/sheet/mineral/metal_hydrogen
+ name = "metallic hydrogen"
+ singular_name = "metallic hydrogen sheet"
+ icon = 'icons/obj/crystallizer_sheets.dmi'
+ icon_state = "sheet-metalhydrogen"
+ merge_type = /obj/item/stack/sheet/mineral/metal_hydrogen
+
+/obj/item/stack/sheet/mineral/zaukerite
+ name = "zaukerite"
+ singular_name = "zaukerite crystal"
+ icon = 'icons/obj/crystallizer_sheets.dmi'
+ icon_state = "zaukerite"
+ merge_type = /obj/item/stack/sheet/mineral/zaukerite
+
+// === Metallic hydrogen crafts ===
+/obj/item/metallic_hydrogen_rod
+ name = "metallic hydrogen rod"
+ desc = "A rod reinforced with metallic hydrogen. Extremely dense; useful as a tool or improvised weapon."
+ icon = 'icons/obj/crystallizer_sheets.dmi'
+ icon_state = "sheet-metalhydrogen"
+ item_state = "rods"
+ force = 10
+ throwforce = 12
+ w_class = WEIGHT_CLASS_NORMAL
+ attack_verb = list("bludgeoned", "hit", "struck")
+
+/obj/item/metallic_hydrogen_cooling_pack
+ name = "metallic hydrogen cooling pack"
+ desc = "Stabilized metallic hydrogen wrapped in cloth. Stays very cold; use to cool down."
+ icon = 'icons/obj/crystallizer_sheets.dmi'
+ icon_state = "sheet-metalhydrogen"
+ w_class = WEIGHT_CLASS_SMALL
+
+/obj/item/metallic_hydrogen_cooling_pack/attack_self(mob/user)
+ . = ..()
+ if(.)
+ return
+ to_chat(user, "You press the pack to your skin; it feels intensely cold. ")
+ if(isliving(user))
+ var/mob/living/L = user
+ L.adjust_bodytemperature(-80)
+
+/obj/item/stack/sheet/hot_ice
+ name = "hot ice"
+ singular_name = "hot ice sheet"
+ icon = 'icons/obj/hot_ice.dmi'
+ icon_state = "hot-ice"
+ merge_type = /obj/item/stack/sheet/hot_ice
+ grind_results = list(/datum/reagent/hot_ice_slush = 25)
+
+/obj/item/stack/sheet/hot_ice/proc/melt_release()
+ var/turf/open/T = get_turf(src)
+ if(!istype(T) || !T.air)
+ return
+ var/plasma_moles = amount * 150
+ var/release_temp = 20 * amount + 300
+ T.atmos_spawn_air("plasma=[plasma_moles];TEMP=[release_temp]")
+ qdel(src)
+
+/obj/item/stack/sheet/hot_ice/fire_act(exposed_temperature, exposed_volume)
+ melt_release()
+
+/obj/item/stack/sheet/hot_ice/welder_act(mob/living/user, obj/item/I)
+ if(I.use_tool(src, user, 0, volume = 50))
+ melt_release()
+ return TRUE
+ return FALSE
+
+// === Hot ice cooling pack (craftable from crystallizer hot ice) ===
+/obj/item/hot_ice_pack
+ name = "hot ice cooling pack"
+ desc = "A pack of stabilized hot ice wrapped in cloth. Stays cold for a long time; use to cool down."
+ icon = 'icons/obj/hot_ice.dmi'
+ icon_state = "hot-ice"
+ w_class = WEIGHT_CLASS_SMALL
+
+/obj/item/hot_ice_pack/attack_self(mob/user)
+ . = ..()
+ if(.)
+ return
+ to_chat(user, "You press the pack to your skin; it feels pleasantly cool. ")
+ if(isliving(user))
+ var/mob/living/L = user
+ L.adjust_bodytemperature(-50)
+
+// === Ammonia pack (craftable from ammonia crystals) ===
+/obj/item/ammonia_pack
+ name = "ammonia pack"
+ desc = "Crystals of ammonia wrapped in cloth. Can be broken to release a small cloud of ammonia."
+ icon = 'icons/obj/crystallizer_sheets.dmi'
+ icon_state = "ammonia_crystal"
+ w_class = WEIGHT_CLASS_SMALL
+
+/obj/item/ammonia_pack/attack_self(mob/user)
+ . = ..()
+ if(.)
+ return
+ var/turf/T = get_turf(src)
+ if(T)
+ var/datum/reagents/R = new(10)
+ R.add_reagent(/datum/reagent/ammonia, 10)
+ var/datum/effect_system/smoke_spread/chem/smoke = new
+ smoke.set_up(R, 1, T, TRUE)
+ smoke.start()
+ to_chat(user, "You crush the pack; a sharp smell of ammonia fills the air. ")
+ qdel(src)
+
+// === Zaukerite bolts (craftable from crystallizer sheets) ===
+/obj/item/zaukerite_bolt
+ name = "zaukerite bolt"
+ desc = "A rod tipped with crystallized Zauker. Deadly when thrown."
+ icon = 'icons/obj/crystallizer_exploration.dmi'
+ icon_state = "zaukerite_bolt"
+ item_state = "bolt"
+ force = 8
+ throwforce = 15
+ throw_speed = 4
+ embedding = list("embedded_pain_multiplier" = 2, "embed_chance" = 40, "embedded_fall_chance" = 10)
+ w_class = WEIGHT_CLASS_SMALL
+ sharpness = SHARP_POINTY
+ attack_verb = list("stabbed", "pierced", "stuck")
+ hitsound = 'sound/weapons/bladeslice.ogg'
diff --git a/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_items.dm b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_items.dm
new file mode 100644
index 0000000000000..7bef3be3616ea
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_items.dm
@@ -0,0 +1,50 @@
+/obj/item/hypernoblium_crystal
+ name = "Hypernoblium Crystal"
+ desc = "Crystallized oxygen and hypernoblium stored in a bottle to pressure-proof your clothes or stop reactions occurring in portable atmospheric devices."
+ icon = 'icons/obj/pipes_n_cables/atmos.dmi'
+ icon_state = "hypernoblium_crystal"
+ var/uses = 1
+
+/obj/item/hypernoblium_crystal/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
+ . = ..()
+ if(!proximity_flag || !uses)
+ return
+ var/obj/item/clothing/worn_item = target
+ if(istype(target, /obj/machinery/portable_atmospherics))
+ to_chat(user, span_notice("You could insert the crystal into [target], but this device does not support hypernoblium crystals."))
+ return
+ if(istype(worn_item))
+ if(istype(worn_item, /obj/item/clothing/suit/space))
+ to_chat(user, span_warning("The [worn_item] is already pressure-resistant!"))
+ return
+ if(worn_item.min_cold_protection_temperature == SPACE_SUIT_MIN_TEMP_PROTECT && worn_item.clothing_flags & STOPSPRESSUREDAMAGE)
+ to_chat(user, span_warning("[worn_item] is already pressure-resistant!"))
+ return
+ to_chat(user, span_notice("You see how the [worn_item] changes color, it's now pressure proof."))
+ worn_item.name = "pressure-resistant [worn_item.name]"
+ worn_item.remove_atom_colour(WASHABLE_COLOUR_PRIORITY)
+ worn_item.add_atom_colour("#00fff7", FIXED_COLOUR_PRIORITY)
+ worn_item.min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
+ worn_item.cold_protection = worn_item.body_parts_covered
+ worn_item.clothing_flags |= STOPSPRESSUREDAMAGE
+ uses--
+ if(uses <= 0)
+ qdel(src)
+ return
+ to_chat(user, span_warning("The crystal can only be used on clothing!"))
+
+/obj/item/nitrium_crystal
+ desc = "A weird brown crystal, it smokes when broken"
+ name = "nitrium crystal"
+ icon = 'icons/obj/pipes_n_cables/atmos.dmi'
+ icon_state = "nitrium_crystal"
+ var/cloud_size = 1
+
+/obj/item/nitrium_crystal/attack_self(mob/user)
+ . = ..()
+ var/datum/effect_system/smoke_spread/smoke = new
+ var/turf/location = get_turf(src)
+ smoke.set_up(cloud_size, location)
+ smoke.attach(location)
+ smoke.start()
+ qdel(src)
diff --git a/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_recipes.dm b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_recipes.dm
new file mode 100644
index 0000000000000..a874f25f2eff9
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer_recipes.dm
@@ -0,0 +1,171 @@
+/// Global list of recipes for atmospheric machines (id -> recipe)
+GLOBAL_LIST_INIT(gas_recipe_meta, gas_recipes_list())
+
+/proc/gas_recipes_list()
+ . = list()
+ for(var/recipe_path in subtypesof(/datum/gas_recipe))
+ var/datum/gas_recipe/recipe = new recipe_path()
+ if(recipe.id != "")
+ .[recipe.id] = recipe
+
+/datum/gas_recipe
+ var/id = ""
+ var/machine_type = ""
+ var/name = ""
+ var/min_temp = TCMB
+ var/max_temp = INFINITY
+ var/energy_release = 0
+ var/dangerous = FALSE
+ /// Gas ID -> moles required (e.g. list(GAS_O2 = 1000, GAS_HYPERNOB = 85))
+ var/list/requirements
+ /// path -> count (e.g. list(/obj/item/hypernoblium_crystal = 1))
+ var/list/products
+
+/datum/gas_recipe/crystallizer
+ machine_type = "Crystallizer"
+
+/datum/gas_recipe/crystallizer/hypern_crystalium
+ id = "hyper_crystalium"
+ name = "Hypernoblium Crystal"
+ min_temp = 3
+ max_temp = 250
+ energy_release = -250000
+ requirements = list(GAS_O2 = 1000, GAS_HYPERNOB = 85)
+ products = list(/obj/item/hypernoblium_crystal = 1)
+
+/datum/gas_recipe/crystallizer/diamond
+ id = "diamond"
+ name = "Diamond"
+ min_temp = 10000
+ max_temp = 30000
+ energy_release = 9500000
+ requirements = list(GAS_CO2 = 1500)
+ products = list(/obj/item/stack/sheet/mineral/diamond = 1)
+
+/datum/gas_recipe/crystallizer/plasma_sheet
+ id = "plasma_sheet"
+ name = "Plasma sheet"
+ min_temp = 10
+ max_temp = 20
+ energy_release = 3500000
+ requirements = list(GAS_PLASMA = 450)
+ products = list(/obj/item/stack/sheet/mineral/plasma = 1)
+
+/datum/gas_recipe/crystallizer/crystallized_nitrium
+ id = "crystallized_nitrium"
+ name = "Nitrium crystal"
+ min_temp = 10
+ max_temp = 25
+ energy_release = -45000
+ requirements = list(GAS_NITRIUM = 150, GAS_O2 = 70, GAS_BZ = 50)
+ products = list(/obj/item/nitrium_crystal = 1)
+
+/datum/gas_recipe/crystallizer/metallic_hydrogen
+ id = "metal_h"
+ name = "Metallic hydrogen"
+ min_temp = 10000 // H2 + BZ catalyst at high heat and pressure (around or above 10,000 K)
+ max_temp = 150000
+ energy_release = -2500000
+ requirements = list(GAS_HYDROGEN = 300, GAS_BZ = 50)
+ products = list(/obj/item/stack/sheet/mineral/metal_hydrogen = 1)
+
+/datum/gas_recipe/crystallizer/healium_grenade
+ id = "healium_g"
+ name = "Healium crystal"
+ min_temp = 200
+ max_temp = 400
+ energy_release = -2000000
+ requirements = list(GAS_HEALIUM = 100, GAS_O2 = 120, GAS_PLASMA = 50)
+ products = list(/obj/item/grenade/gas_crystal/healium_crystal = 1)
+
+/datum/gas_recipe/crystallizer/proto_nitrate_grenade
+ id = "proto_nitrate_g"
+ name = "Proto nitrate crystal"
+ min_temp = 200
+ max_temp = 400
+ energy_release = 1500000
+ requirements = list(GAS_PROTO_NITRATE = 100, GAS_N2 = 80, GAS_O2 = 80)
+ products = list(/obj/item/grenade/gas_crystal/proto_nitrate_crystal = 1)
+
+/datum/gas_recipe/crystallizer/hot_ice
+ id = "hot_ice"
+ name = "Hot ice"
+ min_temp = 15
+ max_temp = 35
+ energy_release = -3000000
+ requirements = list(GAS_FREON = 60, GAS_PLASMA = 160, GAS_O2 = 80)
+ products = list(/obj/item/stack/sheet/hot_ice = 1)
+
+/datum/gas_recipe/crystallizer/ammonia_crystal
+ id = "ammonia_crystal"
+ name = "Ammonia crystal"
+ min_temp = 200
+ max_temp = 240
+ energy_release = 950000
+ requirements = list(GAS_HYDROGEN = 50, GAS_N2 = 40)
+ products = list(/obj/item/stack/ammonia_crystals = 2)
+
+/datum/gas_recipe/crystallizer/shard
+ id = "crystal_shard"
+ name = "Supermatter crystal shard"
+ min_temp = 10
+ max_temp = 20
+ energy_release = 3500000
+ dangerous = TRUE
+ requirements = list(GAS_HYPERNOB = 250, GAS_ANTINOBLIUM = 250, GAS_BZ = 200, GAS_PLASMA = 5000, GAS_O2 = 4500)
+ products = list(/obj/machinery/power/supermatter_crystal/shard = 1)
+
+/datum/gas_recipe/crystallizer/n2o_crystal
+ id = "n2o_crystal"
+ name = "Nitrous oxide crystal"
+ min_temp = 50
+ max_temp = 350
+ energy_release = 3500000
+ requirements = list(GAS_NITROUS = 150, GAS_BZ = 30)
+ products = list(/obj/item/grenade/gas_crystal/nitrous_oxide_crystal = 1)
+
+/datum/gas_recipe/crystallizer/crystal_cell
+ id = "crystal_cell"
+ name = "Crystal Cell"
+ min_temp = 50
+ max_temp = 90
+ energy_release = -800000
+ requirements = list(GAS_PLASMA = 800, GAS_HELIUM = 100, GAS_BZ = 50)
+ products = list(/obj/item/stock_parts/cell/crystal_cell = 1)
+
+/datum/gas_recipe/crystallizer/zaukerite
+ id = "zaukerite"
+ name = "Zaukerite sheet"
+ min_temp = 5
+ max_temp = 20
+ energy_release = 2900000
+ requirements = list(GAS_ANTINOBLIUM = 5, GAS_ZAUKER = 20, GAS_BZ = 8)
+ products = list(/obj/item/stack/sheet/mineral/zaukerite = 2)
+
+/datum/gas_recipe/crystallizer/fuel_pellet
+ id = "fuel_basic"
+ name = "standard fuel pellet"
+ energy_release = -6000000
+ requirements = list(GAS_O2 = 50, GAS_PLASMA = 100)
+ products = list(/obj/item/fuel_pellet = 1)
+
+/datum/gas_recipe/crystallizer/fuel_pellet_advanced
+ id = "fuel_advanced"
+ name = "advanced fuel pellet"
+ energy_release = -6000000
+ requirements = list(GAS_TRITIUM = 100, GAS_HYDROGEN = 100)
+ products = list(/obj/item/fuel_pellet/advanced = 1)
+
+/datum/gas_recipe/crystallizer/fuel_pellet_exotic
+ id = "fuel_exotic"
+ name = "exotic fuel pellet"
+ energy_release = -6000000
+ requirements = list(GAS_HYPERNOB = 100, GAS_NITRIUM = 100)
+ products = list(/obj/item/fuel_pellet/exotic = 1)
+
+/datum/gas_recipe/crystallizer/crystal_foam
+ id = "crystal_foam"
+ name = "Crystal foam grenade"
+ energy_release = 140000
+ requirements = list(GAS_CO2 = 150, GAS_NITROUS = 100, GAS_H2O = 25)
+ products = list(/obj/item/grenade/gas_crystal/crystal_foam = 1)
diff --git a/code/modules/atmospherics/machinery/portable/canister.dm b/code/modules/atmospherics/machinery/portable/canister.dm
index 0cec7b565913c..443ad85945433 100644
--- a/code/modules/atmospherics/machinery/portable/canister.dm
+++ b/code/modules/atmospherics/machinery/portable/canister.dm
@@ -55,12 +55,19 @@
"water vapor" = /obj/machinery/portable_atmospherics/canister/water_vapor,
"tritium" = /obj/machinery/portable_atmospherics/canister/tritium,
"hyper-noblium" = /obj/machinery/portable_atmospherics/canister/nob,
- "stimulum" = /obj/machinery/portable_atmospherics/canister/stimulum,
"pluoxium" = /obj/machinery/portable_atmospherics/canister/pluoxium,
"caution" = /obj/machinery/portable_atmospherics/canister,
"miasma" = /obj/machinery/portable_atmospherics/canister/miasma,
"methane" = /obj/machinery/portable_atmospherics/canister/methane,
- "methyl bromide" = /obj/machinery/portable_atmospherics/canister/methyl_bromide
+ "hydrogen" = /obj/machinery/portable_atmospherics/canister/hydrogen,
+ "helium" = /obj/machinery/portable_atmospherics/canister/helium,
+ "freon" = /obj/machinery/portable_atmospherics/canister/freon,
+ "halon" = /obj/machinery/portable_atmospherics/canister/halon,
+ "antinoblium" = /obj/machinery/portable_atmospherics/canister/antinoblium,
+ "proto nitrate" = /obj/machinery/portable_atmospherics/canister/proto_nitrate,
+ "zauker" = /obj/machinery/portable_atmospherics/canister/zauker,
+ "healium" = /obj/machinery/portable_atmospherics/canister/healium,
+ "nitrium" = /obj/machinery/portable_atmospherics/canister/nitrium
)
/obj/machinery/portable_atmospherics/canister/interact(mob/user)
@@ -129,6 +136,7 @@
icon_state = "brown"
gas_type = GAS_NITRYL
+// Убраны из label2types (не заказываются), но оставлены для совместимости с картами (Academy, ihategordon, undergroundoutpost45)
/obj/machinery/portable_atmospherics/canister/stimulum
name = "stimulum canister"
desc = "Stimulum. High energy gas, high energy people."
@@ -161,12 +169,67 @@
icon_state = "greyblackred"
gas_type = GAS_METHANE
+// Убраны из label2types (не заказываются), оставлены для совместимости с картами (undergroundoutpost45)
/obj/machinery/portable_atmospherics/canister/methyl_bromide
name = "methyl bromide canister"
desc = "Methyl bromide. A potent toxin to most, essential for the Kharmaan to live."
icon_state = "purplecyan"
gas_type = GAS_METHYL_BROMIDE
+/obj/machinery/portable_atmospherics/canister/hydrogen
+ name = "hydrogen canister"
+ desc = "Hydrogen. Flammable and used in fusion."
+ icon_state = "green"
+ gas_type = GAS_HYDROGEN
+
+/obj/machinery/portable_atmospherics/canister/helium
+ name = "helium canister"
+ desc = "Helium. Inert gas, byproduct of fusion."
+ icon_state = "grey"
+ gas_type = GAS_HELIUM
+
+/obj/machinery/portable_atmospherics/canister/freon
+ name = "freon canister"
+ desc = "Freon. Coolant gas. Breathing causes burn damage and slowdown."
+ icon_state = "darkblue"
+ gas_type = GAS_FREON
+
+/obj/machinery/portable_atmospherics/canister/halon
+ name = "halon canister"
+ desc = "Halon. Fire suppressant. Heavy slowdown and heat proof when inhaled."
+ icon_state = "purple"
+ gas_type = GAS_HALON
+
+/obj/machinery/portable_atmospherics/canister/antinoblium
+ name = "antinoblium canister"
+ desc = "Antinoblium. Rare fuel for fusion, replicates by consuming other gases."
+ icon_state = "darkpurple"
+ gas_type = GAS_ANTINOBLIUM
+
+/obj/machinery/portable_atmospherics/canister/proto_nitrate
+ name = "proto nitrate canister"
+ desc = "Proto nitrate. Highly reactive gas, catalyst for many reactions."
+ icon_state = "brown"
+ gas_type = GAS_PROTO_NITRATE
+
+/obj/machinery/portable_atmospherics/canister/zauker
+ name = "zauker canister"
+ desc = "Zauker. Incredibly deadly if inhaled."
+ icon_state = "black"
+ gas_type = GAS_ZAUKER
+
+/obj/machinery/portable_atmospherics/canister/healium
+ name = "healium canister"
+ desc = "Healium. Healing gas, stronger sleeping agent than N2O."
+ icon_state = "red"
+ gas_type = GAS_HEALIUM
+
+/obj/machinery/portable_atmospherics/canister/nitrium
+ name = "nitrium canister"
+ desc = "Nitrium. Gaseous stimulant, enhances speed and endurance."
+ icon_state = "orange"
+ gas_type = GAS_NITRIUM
+
/obj/machinery/portable_atmospherics/canister/proc/get_time_left()
if(timing)
. = round(max(0, valve_timer - world.time) / 10, 1)
diff --git a/code/modules/cargo/exports/large_objects.dm b/code/modules/cargo/exports/large_objects.dm
index 5d9164041f073..a09eba7a6b591 100644
--- a/code/modules/cargo/exports/large_objects.dm
+++ b/code/modules/cargo/exports/large_objects.dm
@@ -170,13 +170,21 @@
cost = 10 //Base cost of canister. You get more for nice gases inside.
unit_name = "Gas Canister"
export_types = list(/obj/machinery/portable_atmospherics/canister)
+ /// Above this many moles in the canister, gas value is scaled down (TG-style overflow penalty)
+ var/canister_mole_threshold = 2000
+ var/canister_overflow_mult = 0.5
/datum/export/large/gas_canister/get_cost(obj/O)
var/obj/machinery/portable_atmospherics/canister/C = O
var/worth = 10
var/list/gas_prices = GLOB.gas_data.prices
+ var/total_moles = C.air_contents.total_moles()
for(var/gas in C.air_contents.get_gases())
- worth += C.air_contents.get_moles(gas)*gas_prices[gas]
+ var/moles = C.air_contents.get_moles(gas)
+ var/gas_worth = moles * (gas_prices[gas] || 0)
+ if(total_moles > canister_mole_threshold)
+ gas_worth *= canister_overflow_mult
+ worth += gas_worth
return worth
diff --git a/code/modules/cargo/exports/sheets.dm b/code/modules/cargo/exports/sheets.dm
index d0e3cbf7c8267..98d7a8c445e23 100644
--- a/code/modules/cargo/exports/sheets.dm
+++ b/code/modules/cargo/exports/sheets.dm
@@ -169,3 +169,78 @@
message = "credits"
export_types = list(/obj/item/stack/telecrystal)
+// Crystallizer products (prices from Whitemoon-station)
+/datum/export/stack/hot_ice
+ cost = CARGO_CRATE_VALUE * 0.8
+ unit_name = "hot ice sheet"
+ message = "of Hot Ice"
+ export_types = list(/obj/item/stack/sheet/hot_ice)
+
+/datum/export/stack/metal_hydrogen
+ cost = CARGO_CRATE_VALUE * 1.05
+ unit_name = "metallic hydrogen sheet"
+ message = "of metallic hydrogen"
+ export_types = list(/obj/item/stack/sheet/mineral/metal_hydrogen)
+
+/datum/export/stack/ammonia_crystals
+ cost = CARGO_CRATE_VALUE * 0.125
+ unit_name = "ammonia crystal"
+ message = "of ammonia crystals"
+ export_types = list(/obj/item/stack/ammonia_crystals)
+
+/datum/export/stack/zaukerite
+ cost = CARGO_CRATE_VALUE * 2
+ unit_name = "zaukerite sheet"
+ message = "of zaukerite"
+ export_types = list(/obj/item/stack/sheet/mineral/zaukerite)
+
+// Crystallizer single-item exports (sellable at cargo)
+/datum/export/crystallizer_item
+ unit_name = "item"
+ include_subtypes = FALSE
+
+/datum/export/crystallizer_item/nitrium_crystal
+ cost = CARGO_CRATE_VALUE * 2.5
+ unit_name = "nitrium crystal"
+ export_types = list(/obj/item/nitrium_crystal)
+
+/datum/export/crystallizer_item/hypernoblium_crystal
+ cost = CARGO_CRATE_VALUE * 4
+ unit_name = "hypernoblium crystal"
+ export_types = list(/obj/item/hypernoblium_crystal)
+
+/datum/export/crystallizer_item/fuel_pellet
+ cost = CARGO_CRATE_VALUE * 0.25
+ unit_name = "fuel pellet"
+ export_types = list(/obj/item/fuel_pellet)
+
+/datum/export/crystallizer_item/fuel_pellet/advanced
+ cost = CARGO_CRATE_VALUE * 0.5
+ unit_name = "advanced fuel pellet"
+ export_types = list(/obj/item/fuel_pellet/advanced)
+
+/datum/export/crystallizer_item/fuel_pellet/exotic
+ cost = CARGO_CRATE_VALUE * 0.75
+ unit_name = "exotic fuel pellet"
+ export_types = list(/obj/item/fuel_pellet/exotic)
+
+/datum/export/crystallizer_item/gas_crystal_healium
+ cost = CARGO_CRATE_VALUE * 1.5
+ unit_name = "healium crystal"
+ export_types = list(/obj/item/grenade/gas_crystal/healium_crystal)
+
+/datum/export/crystallizer_item/gas_crystal_proto_nitrate
+ cost = CARGO_CRATE_VALUE * 1
+ unit_name = "proto nitrate crystal"
+ export_types = list(/obj/item/grenade/gas_crystal/proto_nitrate_crystal)
+
+/datum/export/crystallizer_item/gas_crystal_n2o
+ cost = CARGO_CRATE_VALUE * 0.5
+ unit_name = "N2O crystal"
+ export_types = list(/obj/item/grenade/gas_crystal/nitrous_oxide_crystal)
+
+/datum/export/crystallizer_item/gas_crystal_foam
+ cost = CARGO_CRATE_VALUE * 1
+ unit_name = "crystal foam"
+ export_types = list(/obj/item/grenade/gas_crystal/crystal_foam)
+
diff --git a/code/modules/cargo/packs/engine.dm b/code/modules/cargo/packs/engine.dm
index 10b520dbce1d5..3a9950e7438ce 100644
--- a/code/modules/cargo/packs/engine.dm
+++ b/code/modules/cargo/packs/engine.dm
@@ -196,6 +196,31 @@
dangerous = TRUE
contraband = TRUE
+/datum/supply_pack/engine/crystallizer
+ name = "Crystallizer Board"
+ desc = "Плата для сборки кристаллярия — машины для кристаллизации и отверждения газов. Компоненты и трубы в комплект не входят."
+ cost = 4000
+ contains = list(/obj/item/circuitboard/machine/crystallizer)
+ crate_name = "crystallizer board crate"
+
+/datum/supply_pack/engine/hfr
+ name = "Hypertorus Fusion Reactor Kit"
+ desc = "Набор для сборки реактора термоядерного синтеза (ХФР). Содержит ядро, четыре угловых блока, порты подачи топлива и модулятора, отвода отходов и интерфейс. Разместите боксы в сетке 3x3 и активируйте мультитулом на ядре. Требуется доступ СЕ."
+ cost = 15000
+ access = ACCESS_CE
+ contains = list(/obj/item/hfr_box/core,
+ /obj/item/hfr_box/corner,
+ /obj/item/hfr_box/corner,
+ /obj/item/hfr_box/corner,
+ /obj/item/hfr_box/corner,
+ /obj/item/hfr_box/body/fuel_input,
+ /obj/item/hfr_box/body/moderator_input,
+ /obj/item/hfr_box/body/waste_output,
+ /obj/item/hfr_box/body/interface)
+ crate_name = "Hypertorus Fusion Reactor Kit"
+ crate_type = /obj/structure/closet/crate/secure/engineering
+ dangerous = TRUE
+
/datum/supply_pack/engine/reactor
name = "RMBK Nuclear Reactor Kit" // (not) a toy
desc = "Содержит реакторный маяк и 3 реакторных пульта. Урановые стержни в комплект не входят."
diff --git a/code/modules/clothing/head/helmet.dm b/code/modules/clothing/head/helmet.dm
index e78a2f8fbd54e..ad00cc93cffd9 100644
--- a/code/modules/clothing/head/helmet.dm
+++ b/code/modules/clothing/head/helmet.dm
@@ -672,3 +672,22 @@
icon_state = "policehelm"
dynamic_hair_suffix = ""
flags_inv = HIDEEARS
+
+// Elder Atmosian — шлем легендарного атмос-теха (спрайты из WhiteMoon modular_zubbers)
+/obj/item/clothing/head/helmet/elder_atmosian
+ name = "\improper Elder Atmosian Helmet"
+ desc = "Вершина атмос-экипировки: огнезащитный шлем, усиленный металлическим водородом. Полная защита головы от огня и газов."
+ icon = 'modular_bluemoon/icons/obj/clothing/head/helmet.dmi'
+ mob_overlay_icon = 'modular_bluemoon/icons/mob/clothing/head/helmet.dmi'
+ anthro_mob_worn_overlay = 'modular_bluemoon/icons/mob/clothing/head/helmet_teshari.dmi'
+ icon_state = "h2helmet"
+ item_state = "h2_helmet"
+ armor = list(MELEE = 50, BULLET = 45, LASER = 55, ENERGY = 55, BOMB = 95, BIO = 100, RAD = 100, FIRE = 100, ACID = 90, WOUND = 30)
+ flags_inv = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT
+ flags_cover = HEADCOVERSEYES | HEADCOVERSMOUTH
+ cold_protection = HEAD
+ heat_protection = HEAD
+ min_cold_protection_temperature = SPACE_HELM_MIN_TEMP_PROTECT
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ clothing_flags = STOPSPRESSUREDAMAGE | THICKMATERIAL | BLOCK_GAS_SMOKE_EFFECT
+ resistance_flags = FIRE_PROOF
diff --git a/code/modules/clothing/suits/armor.dm b/code/modules/clothing/suits/armor.dm
index 0a4691bd9997a..ea57d19b66057 100644
--- a/code/modules/clothing/suits/armor.dm
+++ b/code/modules/clothing/suits/armor.dm
@@ -354,7 +354,7 @@
body_parts_covered = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
allowed = list(/obj/item/gun/energy, /obj/item/melee/baton, /obj/item/restraints/handcuffs, /obj/item/tank/internals/emergency_oxygen, /obj/item/tank/internals/plasmaman)
clothing_flags = THICKMATERIAL
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
cold_protection = CHEST | GROIN | LEGS | FEET | ARMS | HANDS
min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
heat_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
@@ -371,12 +371,12 @@
clothing_flags = THICKMATERIAL
body_parts_covered = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
slowdown = 3
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
armor = list(MELEE = 80, BULLET = 80, LASER = 50, ENERGY = 50, BOMB = 100, BIO = 100, RAD = 100, FIRE = 90, ACID = 90, WOUND = 50)
/obj/item/clothing/suit/armor/tdome
body_parts_covered = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|HIDETAUR
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|HIDETAUR|ALLOWS_BACK_TANK
clothing_flags = THICKMATERIAL
cold_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
heat_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
@@ -403,7 +403,7 @@
item_state = "knight_green"
armor = list(MELEE = 80, BULLET = 40, LASER = 10, ENERGY = 10, BOMB = 10, BIO = 0, RAD = 0, FIRE = 80, ACID = 80, WOUND = 30)
slowdown = 0.5
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
/obj/item/clothing/suit/armor/riot/knight/yellow
icon_state = "knight_yellow"
@@ -478,3 +478,36 @@
icon_state = "sov_offcoat"
item_state = "sov_offcoat"
//armor = list(MELEE = 25, BULLET = 20, LASER = 20, ENERGY = 10, BOMB = 20, BIO = 50, RAD = 50, FIRE = -10, ACID = 50, WOUND = 10)
+
+// Elder Atmosian — риг легендарного атмос-теха
+/obj/item/clothing/suit/armor/elder_atmosian
+ name = "\improper Elder Atmosian Armor"
+ desc = "Вершина атмос-экипировки: дорогая огнезащитная броня, усиленная металлическим водородом. Полная защита от огня и газов без тяжёлого замедления. В слот костюма можно повесить металл-водородный топор."
+ icon = 'modular_bluemoon/icons/obj/clothing/suits/armor.dmi'
+ mob_overlay_icon = 'modular_bluemoon/icons/mob/clothing/suits/armor.dmi'
+ anthro_mob_worn_overlay = 'modular_bluemoon/icons/mob/clothing/suits/armor_digi.dmi'
+ taur_mob_worn_overlay = 'modular_bluemoon/icons/mob/clothing/suits/armor_teshari.dmi'
+ icon_state = "h2armor"
+ item_state = null
+ material_flags = MATERIAL_ADD_PREFIX | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS
+ armor = list(MELEE = 50, BULLET = 45, LASER = 55, ENERGY = 55, BOMB = 95, BIO = 100, RAD = 100, FIRE = 100, ACID = 90, WOUND = 30)
+ body_parts_covered = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
+ cold_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
+ heat_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
+ min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
+ max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
+ gas_transfer_coefficient = 0.01
+ permeability_coefficient = 0.01
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|HIDETAUR|ALLOWS_BACK_TANK
+ clothing_flags = STOPSPRESSUREDAMAGE | THICKMATERIAL
+ strip_delay = 60
+ equip_delay_other = 60
+ resistance_flags = FIRE_PROOF
+
+/obj/item/clothing/suit/armor/elder_atmosian/Initialize(mapload)
+ . = ..()
+ allowed = islist(allowed) ? allowed.Copy() : list()
+ allowed += list(
+ /obj/item/fireaxe,
+ /obj/item/tank,
+ )
diff --git a/code/modules/clothing/suits/jobs.dm b/code/modules/clothing/suits/jobs.dm
index ff077d7fcde0b..7ffb3b8a04ff9 100644
--- a/code/modules/clothing/suits/jobs.dm
+++ b/code/modules/clothing/suits/jobs.dm
@@ -20,7 +20,7 @@
icon_state = "captunic"
item_state = "bio_suit"
body_parts_covered = CHEST|GROIN|LEGS|ARMS
- flags_inv = HIDEJUMPSUIT
+ flags_inv = HIDEJUMPSUIT|ALLOWS_BACK_TANK
allowed = list(/obj/item/disk, /obj/item/stamp, /obj/item/reagent_containers/food/drinks/flask, /obj/item/melee, /obj/item/storage/lockbox/medal, /obj/item/assembly/flash/handheld, /obj/item/storage/box/matches, /obj/item/lighter, /obj/item/clothing/mask/cigarette, /obj/item/storage/fancy/cigarettes, /obj/item/tank/internals/emergency_oxygen, /obj/item/tank/internals/plasmaman)
//Chaplain
@@ -38,7 +38,7 @@
//icon_state = "nun"
//item_state = "nun"
body_parts_covered = CHEST|GROIN|LEGS|ARMS|HANDS
- flags_inv = HIDEJUMPSUIT
+ flags_inv = HIDEJUMPSUIT|ALLOWS_BACK_TANK
mutantrace_variation = STYLE_DIGITIGRADE|STYLE_NO_ANTHRO_ICON
/obj/item/clothing/suit/chaplain/studentuni
@@ -70,7 +70,7 @@
icon_state = "holidaypriest"
item_state = "w_suit"
body_parts_covered = CHEST|GROIN|LEGS|ARMS
- flags_inv = HIDEJUMPSUIT
+ flags_inv = HIDEJUMPSUIT|ALLOWS_BACK_TANK
mutantrace_variation = STYLE_DIGITIGRADE|STYLE_NO_ANTHRO_ICON
diff --git a/code/modules/clothing/suits/miscellaneous.dm b/code/modules/clothing/suits/miscellaneous.dm
index 32f5a6c0a893e..e37835d1dc960 100644
--- a/code/modules/clothing/suits/miscellaneous.dm
+++ b/code/modules/clothing/suits/miscellaneous.dm
@@ -72,7 +72,7 @@
flags_1 = CONDUCT_1
fire_resist = T0C+5200
body_parts_covered = CHEST|GROIN|LEGS|ARMS|HANDS|FEET
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
/obj/item/clothing/suit/justice
@@ -81,7 +81,7 @@
icon_state = "justice"
item_state = "justice"
body_parts_covered = CHEST|GROIN|LEGS|ARMS|HANDS|FEET
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
/obj/item/clothing/suit/judgerobe
@@ -91,7 +91,7 @@
item_state = "judge"
body_parts_covered = CHEST|GROIN|LEGS|ARMS
allowed = list(/obj/item/storage/fancy/cigarettes, /obj/item/stack/spacecash)
- flags_inv = HIDEJUMPSUIT
+ flags_inv = HIDEJUMPSUIT|ALLOWS_BACK_TANK
mutantrace_variation = STYLE_DIGITIGRADE|STYLE_NO_ANTHRO_ICON
/obj/item/clothing/suit/tailcoat
@@ -137,7 +137,7 @@
desc = "A plastic replica of the Syndicate space suit. You'll look just like a real murderous Syndicate agent in this! This is a toy, it is not made for use in space!"
body_parts_covered = CHEST|ARMS|GROIN|LEGS|FEET|HANDS
allowed = list(/obj/item/flashlight, /obj/item/tank/internals/emergency_oxygen, /obj/item/tank/internals/plasmaman, /obj/item/toy)
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
resistance_flags = NONE
/obj/item/clothing/suit/hastur
@@ -146,7 +146,7 @@
icon_state = "hastur"
item_state = "hastur"
body_parts_covered = CHEST|GROIN|LEGS|ARMS
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
mutantrace_variation = NONE
/obj/item/clothing/suit/imperium_monk
@@ -155,7 +155,7 @@
icon_state = "imperium_monk"
item_state = "imperium_monk"
body_parts_covered = CHEST|GROIN|LEGS|ARMS
- flags_inv = HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
allowed = list(/obj/item/storage/book/bible, /obj/item/nullrod, /obj/item/reagent_containers/food/drinks/bottle/holywater, /obj/item/storage/fancy/candle_box, /obj/item/candle, /obj/item/tank/internals/emergency_oxygen)
mutantrace_variation = NONE
@@ -165,7 +165,7 @@
icon_state = "chickensuit"
item_state = "chickensuit"
body_parts_covered = CHEST|ARMS|GROIN|LEGS|FEET
- flags_inv = HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
/obj/item/clothing/suit/monkeysuit
@@ -174,7 +174,7 @@
icon_state = "monkeysuit"
item_state = "monkeysuit"
body_parts_covered = CHEST|ARMS|GROIN|LEGS|FEET|HANDS
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
/obj/item/clothing/suit/toggle/owlwings
name = "owl cloak"
@@ -201,7 +201,7 @@
icon_state = "cardborg"
item_state = "cardborg"
body_parts_covered = CHEST|GROIN
- flags_inv = HIDEJUMPSUIT
+ flags_inv = HIDEJUMPSUIT|ALLOWS_BACK_TANK
dog_fashion = /datum/dog_fashion/back
/obj/item/clothing/suit/cardborg/equipped(mob/living/user, slot)
@@ -230,7 +230,7 @@
icon_state = "snowman"
item_state = "snowman"
body_parts_covered = CHEST|GROIN
- flags_inv = HIDEJUMPSUIT
+ flags_inv = HIDEJUMPSUIT|ALLOWS_BACK_TANK
mutantrace_variation = STYLE_DIGITIGRADE|STYLE_NO_ANTHRO_ICON
/obj/item/clothing/suit/poncho
@@ -269,7 +269,7 @@
icon_state = "white_dress"
item_state = "w_suit"
body_parts_covered = CHEST|GROIN|LEGS|FEET
- flags_inv = HIDEJUMPSUIT|HIDESHOES
+ flags_inv = HIDEJUMPSUIT|HIDESHOES|ALLOWS_BACK_TANK
mutantrace_variation = NONE
/obj/item/clothing/suit/hooded/carp_costume
@@ -395,7 +395,7 @@
strip_delay = 120
slowdown = 0.5
obj_flags = IMMUTABLE_SLOW
- flags_inv = HIDESHOES|HIDEJUMPSUIT|HIDETAUR
+ flags_inv = HIDESHOES|HIDEJUMPSUIT|HIDETAUR|ALLOWS_BACK_TANK
/obj/item/clothing/suit/ran
name = "shikigami costume"
@@ -403,7 +403,7 @@
icon_state = "ran_suit"
item_state = "ran_suit"
body_parts_covered = CHEST|GROIN|LEGS
- flags_inv = HIDEJUMPSUIT|HIDETAUR
+ flags_inv = HIDEJUMPSUIT|HIDETAUR|ALLOWS_BACK_TANK
cold_protection = CHEST|GROIN|LEGS //fluffy tails!
min_cold_protection_temperature = FIRE_SUIT_MIN_TEMP_PROTECT //Bleh, same as winter coat
heat_protection = CHEST|GROIN|LEGS
@@ -427,7 +427,7 @@
icon_state = "straight_jacket"
item_state = "straight_jacket"
body_parts_covered = CHEST|GROIN|LEGS|ARMS|HANDS
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
equip_delay_self = 50
strip_delay = 60
breakouttime = 3000
@@ -664,7 +664,7 @@
icon_state = "xenos"
item_state = "xenos_helm"
body_parts_covered = CHEST|GROIN|LEGS|ARMS|HANDS
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
allowed = list(/obj/item/clothing/mask/facehugger/toy)
/obj/item/clothing/suit/caution
@@ -1208,7 +1208,7 @@
item_state = "assu_suit"
blood_overlay_type = "armor"
body_parts_covered = CHEST|GROIN|ARMS|LEGS
- flags_inv = HIDEJUMPSUIT
+ flags_inv = HIDEJUMPSUIT|ALLOWS_BACK_TANK
resistance_flags = NONE
/obj/item/clothing/suit/hooded/wintercoat/christmascoatr
diff --git a/code/modules/clothing/suits/utility.dm b/code/modules/clothing/suits/utility.dm
index 9da2692fad442..bcbe48bb2c685 100644
--- a/code/modules/clothing/suits/utility.dm
+++ b/code/modules/clothing/suits/utility.dm
@@ -22,7 +22,7 @@
allowed = list(/obj/item/flashlight, /obj/item/tank/internals/emergency_oxygen, /obj/item/tank/internals/plasmaman, /obj/item/extinguisher, /obj/item/crowbar, /obj/item/tank/internals/oxygen, /obj/item/tank/internals/air, /obj/item/tank/internals/generic)
slowdown = 1
armor = list(MELEE = 15, BULLET = 5, LASER = 20, ENERGY = 10, BOMB = 20, BIO = 10, RAD = 20, FIRE = 100, ACID = 50)
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|HIDETAUR
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|HIDETAUR|ALLOWS_BACK_TANK
clothing_flags = STOPSPRESSUREDAMAGE | THICKMATERIAL
heat_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT
@@ -36,7 +36,7 @@
icon_state = "firesuit"
item_state = "firefighter"
tail_state = "atmos"
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|HIDETAUR
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|HIDETAUR|ALLOWS_BACK_TANK
mutantrace_variation = STYLE_DIGITIGRADE|STYLE_SNEK_TAURIC|STYLE_PAW_TAURIC
@@ -58,7 +58,7 @@
cold_protection = CHEST | GROIN | LEGS | FEET | ARMS | HANDS
min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|HIDETAUR
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|HIDETAUR|ALLOWS_BACK_TANK
mutantrace_variation = STYLE_DIGITIGRADE|STYLE_SNEK_TAURIC|STYLE_PAW_TAURIC
/*
diff --git a/code/modules/clothing/suits/wiz_robe.dm b/code/modules/clothing/suits/wiz_robe.dm
index 3ec5e373de67f..ce1a79da7486d 100644
--- a/code/modules/clothing/suits/wiz_robe.dm
+++ b/code/modules/clothing/suits/wiz_robe.dm
@@ -78,7 +78,7 @@
body_parts_covered = CHEST|ARMS|LEGS
armor = list(MELEE = 30, BULLET = 20, LASER = 20, ENERGY = 20, BOMB = 20, BIO = 20, RAD = 20, FIRE = 100, ACID = 100, WOUND = 20)
allowed = list(/obj/item/teleportation_scroll)
- flags_inv = HIDEJUMPSUIT
+ flags_inv = HIDEJUMPSUIT|ALLOWS_BACK_TANK
strip_delay = 50
equip_delay_other = 50
resistance_flags = FIRE_PROOF | ACID_PROOF
diff --git a/code/modules/holiday/easter.dm b/code/modules/holiday/easter.dm
index dc0bc66b1d106..20c9a27dba27e 100644
--- a/code/modules/holiday/easter.dm
+++ b/code/modules/holiday/easter.dm
@@ -104,7 +104,7 @@
item_state = "bunnysuit"
slowdown = -1
body_parts_covered = CHEST|GROIN|LEGS|ARMS
- flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
+ flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT|ALLOWS_BACK_TANK
//Egg prizes and egg spawns!
/obj/item/reagent_containers/food/snacks/egg
diff --git a/code/modules/mining/equipment/explorer_gear.dm b/code/modules/mining/equipment/explorer_gear.dm
index cc13de651716a..ecb7ca9b7a254 100644
--- a/code/modules/mining/equipment/explorer_gear.dm
+++ b/code/modules/mining/equipment/explorer_gear.dm
@@ -11,7 +11,7 @@
min_cold_protection_temperature = FIRE_SUIT_MIN_TEMP_PROTECT
hoodtype = /obj/item/clothing/head/hooded/explorer
armor = list(MELEE = 30, BULLET = 20, LASER = 20, ENERGY = 20, BOMB = 50, BIO = 100, RAD = 50, FIRE = 50, ACID = 50, WOUND = 15)
- flags_inv = HIDEJUMPSUIT|HIDETAUR
+ flags_inv = HIDEJUMPSUIT|HIDETAUR|ALLOWS_BACK_TANK
allowed = list(/obj/item/flashlight, /obj/item/tank/internals, /obj/item/resonator, /obj/item/mining_scanner, /obj/item/t_scanner/adv_mining_scanner, /obj/item/gun/energy/kinetic_accelerator, /obj/item/pickaxe, /obj/item/device/cooler/lavaland) // BLUEMOON ADD - добавлен лавалендовский ПОУ
resistance_flags = FIRE_PROOF
mutantrace_variation = STYLE_DIGITIGRADE|STYLE_SNEK_TAURIC|STYLE_PAW_TAURIC
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 719ec2bfa584d..cb3b5f097ac2b 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -385,6 +385,9 @@
if(wear_suit.flags_inv & HIDESHOES)
LAZYOR(., ITEM_SLOT_FEET)
LAZYOR(., ITEM_SLOT_SOCKS)
+ // Full-body suits HIDEJUMPSUIT normally obscure back; ALLOWS_BACK_TANK or no HIDEBACK allows tank on back
+ if((wear_suit.flags_inv & HIDEBACK) || ((wear_suit.flags_inv & HIDEJUMPSUIT) && !(wear_suit.flags_inv & ALLOWS_BACK_TANK)))
+ LAZYOR(., ITEM_SLOT_BACK)
if(w_uniform)
if(underwear_hidden())
LAZYOR(., ITEM_SLOT_UNDERWEAR)
diff --git a/code/modules/power/cell.dm b/code/modules/power/cell.dm
index a03c3b6059049..aadd42b1b53ec 100644
--- a/code/modules/power/cell.dm
+++ b/code/modules/power/cell.dm
@@ -261,6 +261,16 @@
name = "pulse pistol power cell"
maxcharge = 2000
+/obj/item/stock_parts/cell/crystal_cell
+ name = "crystal power cell"
+ desc = "A very high power cell made from crystallized plasma."
+ icon_state = "crystal_cell"
+ maxcharge = 50000
+ chargerate = 0
+ has_charge_overlay = FALSE
+ custom_materials = null
+ grind_results = null
+
/obj/item/stock_parts/cell/high
name = "high-capacity power cell"
icon_state = "hcell"
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index cbf858b4a72b9..62b789933e1c2 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -1697,6 +1697,51 @@
L.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/nitryl)
..()
+/datum/reagent/freon
+ name = "Freon"
+ description = "A coolant gas. Breathing it causes burn damage and heavy slowdown."
+ reagent_state = GAS
+ gas = GAS_FREON
+ metabolization_rate = REAGENTS_METABOLISM
+ color = "#66ccff"
+ taste_description = "cold burn"
+ value = REAGENT_VALUE_UNCOMMON
+
+/datum/reagent/freon/on_mob_metabolize(mob/living/L)
+ ..()
+ L.add_movespeed_modifier(/datum/movespeed_modifier/reagent/freon)
+
+/datum/reagent/freon/on_mob_end_metabolize(mob/living/L)
+ L.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/freon)
+ ..()
+
+/datum/reagent/halon
+ name = "Halon"
+ description = "A fire suppressant gas. Heavy slowdown when inhaled, but makes you heat proof."
+ reagent_state = GAS
+ gas = GAS_HALON
+ metabolization_rate = REAGENTS_METABOLISM
+ color = "#44cc44"
+ taste_description = "chemical stagnation"
+ value = REAGENT_VALUE_UNCOMMON
+
+/datum/reagent/halon/on_mob_metabolize(mob/living/L)
+ ..()
+ L.add_movespeed_modifier(/datum/movespeed_modifier/reagent/halon)
+ ADD_TRAIT(L, TRAIT_RESISTHEAT, type)
+
+/datum/reagent/halon/on_mob_end_metabolize(mob/living/L)
+ L.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/halon)
+ REMOVE_TRAIT(L, TRAIT_RESISTHEAT, type)
+ ..()
+
+/datum/reagent/hot_ice_slush
+ name = "Hot Ice Slush"
+ description = "A slush of hot ice. Holds a great amount of power inside."
+ color = "#66ccff"
+ taste_description = "cold burn"
+ value = REAGENT_VALUE_VERY_RARE
+
/////////////////////////Coloured Crayon Powder////////////////////////////
//For colouring in /proc/mix_color_from_reagents
diff --git a/code/modules/research/designs/machine_desings/machine_designs_all_misc.dm b/code/modules/research/designs/machine_desings/machine_designs_all_misc.dm
index 8da5aba0578b1..4532a0a384310 100644
--- a/code/modules/research/designs/machine_desings/machine_designs_all_misc.dm
+++ b/code/modules/research/designs/machine_desings/machine_designs_all_misc.dm
@@ -77,6 +77,14 @@
category = list ("Engineering Machinery")
departmental_flags = DEPARTMENTAL_FLAG_ALL
+/datum/design/board/electrolyzer
+ name = "Machine Design (Electrolyzer Board)"
+ desc = "The circuit board for a space electrolyzer."
+ id = "electrolyzer"
+ build_path = /obj/item/circuitboard/machine/electrolyzer
+ category = list ("Engineering Machinery")
+ departmental_flags = DEPARTMENTAL_FLAG_ALL
+
/datum/design/board/reagentgrinder
name = "Machine Design (All-In-One Grinder)"
desc = "The circuit board for an All-In-One Grinder."
diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm
index 61a0a27ae443f..ea1eed155610c 100644
--- a/code/modules/research/techweb/_techweb_node.dm
+++ b/code/modules/research/techweb/_techweb_node.dm
@@ -104,7 +104,7 @@
// Default research tech, prevents bricking
design_ids = list("basic_matter_bin", "basic_cell", "basic_scanning", "basic_capacitor", "basic_micro_laser", "micro_mani", "desttagger", "handlabel", "packagewrap",
"destructive_analyzer", "circuit_imprinter", "circuit_imprinter_science", "circuit_imprinter_robotic", "experimentor", "rdconsole", "bepis", "design_disk", "tech_disk", "rdserver", "rdservercontrol", "mechfab", "paystand",
- "space_heater", "beaker", "large_beaker", "xlarge_beaker", "bucket", "hypovial", "large_hypovial", "syringe", "pillbottle",
+ "space_heater", "electrolyzer", "beaker", "large_beaker", "xlarge_beaker", "bucket", "hypovial", "large_hypovial", "syringe", "pillbottle",
"sec_beanbag", "sec_rshot", "sec_bshot", "sec_slug", "sec_islug", "sec_dart", "sec_38", "sec_38lethal",
"rglass","plasteel","plastitanium","plasmaglass","plasmareinforcedglass","titaniumglass","plastitaniumglass", "salestagger",
"cooler_mining", "cooler")
diff --git a/code/modules/surgery/organs/lungs.dm b/code/modules/surgery/organs/lungs.dm
index b92a9dbe39cb4..35bbcc7c59e9e 100644
--- a/code/modules/surgery/organs/lungs.dm
+++ b/code/modules/surgery/organs/lungs.dm
@@ -57,7 +57,7 @@
var/SA_para_min = 1 //nitrous values
var/SA_sleep_min = 5
var/BZ_trip_balls_min = 0.1 //BZ gas
- var/BZ_brain_damage_min = 1
+ var/BZ_brain_damage_min = 10 // partial pressure over 10: 33% per tick for 3 brain damage, max 150
var/gas_stimulation_min = 0.002 //Nitryl and Stimulum
var/cold_message = "your face freezing and an icicle forming"
@@ -309,6 +309,17 @@
breath.adjust_moles(GAS_NITRYL, -gas_breathed)
+ // Freon — burn damage + slowdown (reagent from breath_reagent)
+ gas_breathed = breath.get_moles(GAS_FREON)
+ if (gas_breathed > 0.0001)
+ var/freon_pp = PP(breath, GAS_FREON)
+ H.adjustFireLoss(clamp(round(freon_pp * 0.5), 0, 5))
+
+ // Nitrium — at high concentrations causes lung damage
+ var/nitrium_pp = PP(breath, GAS_NITRIUM)
+ if (nitrium_pp > 20)
+ applyOrganDamage(min(round((nitrium_pp - 20) * 0.5), 15))
+
// Stimulum
gas_breathed = PP(breath,GAS_STIMULUM)
if (gas_breathed > gas_stimulation_min)
@@ -316,6 +327,17 @@
H.reagents.add_reagent(/datum/reagent/stimulum, max(0, 5 - existing))
breath.adjust_moles(GAS_STIMULUM, -gas_breathed)
+ // Healium — лечит брутал и берн при дыхании (не breath_reagent: иначе газ вычитается до этого блока и лечение не срабатывает)
+ gas_breathed = breath.get_moles(GAS_HEALIUM)
+ if (gas_breathed > 0.0001)
+ var/healium_pp = PP(breath, GAS_HEALIUM)
+ var/heal_amount = clamp(round(healium_pp * 6), 3, 18)
+ H.adjustBruteLoss(-heal_amount)
+ H.adjustFireLoss(-heal_amount)
+ H.adjustOxyLoss(-max(round(heal_amount * 0.5), 1))
+ H.adjustToxLoss(-max(round(heal_amount * 0.3), 1))
+ breath.adjust_moles(GAS_HEALIUM, -gas_breathed)
+
// Miasma
if (breath.get_moles(GAS_MIASMA))
var/miasma_pp = PP(breath,GAS_MIASMA)
diff --git a/icons/obj/crystallizer_exploration.dmi b/icons/obj/crystallizer_exploration.dmi
new file mode 100644
index 0000000000000..b7224d2df84d9
Binary files /dev/null and b/icons/obj/crystallizer_exploration.dmi differ
diff --git a/icons/obj/crystallizer_grenades.dmi b/icons/obj/crystallizer_grenades.dmi
new file mode 100644
index 0000000000000..415166f9c4074
Binary files /dev/null and b/icons/obj/crystallizer_grenades.dmi differ
diff --git a/icons/obj/crystallizer_sheets.dmi b/icons/obj/crystallizer_sheets.dmi
new file mode 100644
index 0000000000000..a10d56851ada4
Binary files /dev/null and b/icons/obj/crystallizer_sheets.dmi differ
diff --git a/icons/obj/exploration.dmi b/icons/obj/exploration.dmi
new file mode 100644
index 0000000000000..b7224d2df84d9
Binary files /dev/null and b/icons/obj/exploration.dmi differ
diff --git a/icons/obj/hot_ice.dmi b/icons/obj/hot_ice.dmi
new file mode 100644
index 0000000000000..a10d56851ada4
Binary files /dev/null and b/icons/obj/hot_ice.dmi differ
diff --git a/icons/obj/machines/atmospherics/binary_devices.dmi b/icons/obj/machines/atmospherics/binary_devices.dmi
new file mode 100644
index 0000000000000..7855922700cdb
Binary files /dev/null and b/icons/obj/machines/atmospherics/binary_devices.dmi differ
diff --git a/icons/obj/machines/atmospherics/hypertorus.dmi b/icons/obj/machines/atmospherics/hypertorus.dmi
new file mode 100644
index 0000000000000..1c7107cf59bcd
Binary files /dev/null and b/icons/obj/machines/atmospherics/hypertorus.dmi differ
diff --git a/icons/obj/machines/atmospherics/machines.dmi b/icons/obj/machines/atmospherics/machines.dmi
new file mode 100644
index 0000000000000..baf01a3e9cb85
Binary files /dev/null and b/icons/obj/machines/atmospherics/machines.dmi differ
diff --git a/icons/obj/machines/atmospherics/miners.dmi b/icons/obj/machines/atmospherics/miners.dmi
new file mode 100644
index 0000000000000..db47b47feeadb
Binary files /dev/null and b/icons/obj/machines/atmospherics/miners.dmi differ
diff --git a/icons/obj/machines/atmospherics/thermomachine.dmi b/icons/obj/machines/atmospherics/thermomachine.dmi
new file mode 100644
index 0000000000000..bcc36f4242f15
Binary files /dev/null and b/icons/obj/machines/atmospherics/thermomachine.dmi differ
diff --git a/icons/obj/machines/atmospherics/trinary_devices.dmi b/icons/obj/machines/atmospherics/trinary_devices.dmi
new file mode 100644
index 0000000000000..3f65aa419aaa2
Binary files /dev/null and b/icons/obj/machines/atmospherics/trinary_devices.dmi differ
diff --git a/icons/obj/machines/atmospherics/unary_devices.dmi b/icons/obj/machines/atmospherics/unary_devices.dmi
new file mode 100644
index 0000000000000..73da54768b1cb
Binary files /dev/null and b/icons/obj/machines/atmospherics/unary_devices.dmi differ
diff --git a/icons/obj/pipes_n_cables/atmos.dmi b/icons/obj/pipes_n_cables/atmos.dmi
new file mode 100644
index 0000000000000..e7401411a290e
Binary files /dev/null and b/icons/obj/pipes_n_cables/atmos.dmi differ
diff --git a/icons/obj/pipes_n_cables/canisters.dmi b/icons/obj/pipes_n_cables/canisters.dmi
new file mode 100644
index 0000000000000..d7761182d8a52
Binary files /dev/null and b/icons/obj/pipes_n_cables/canisters.dmi differ
diff --git a/icons/obj/pipes_n_cables/pipe_underlays.dmi b/icons/obj/pipes_n_cables/pipe_underlays.dmi
new file mode 100644
index 0000000000000..1a74cc3740190
Binary files /dev/null and b/icons/obj/pipes_n_cables/pipe_underlays.dmi differ
diff --git a/icons/obj/pipes_n_cables/stationary_canisters_misc.dmi b/icons/obj/pipes_n_cables/stationary_canisters_misc.dmi
new file mode 100644
index 0000000000000..857b5a816b40c
Binary files /dev/null and b/icons/obj/pipes_n_cables/stationary_canisters_misc.dmi differ
diff --git a/icons/obj/weapons/fireaxe.dmi b/icons/obj/weapons/fireaxe.dmi
new file mode 100644
index 0000000000000..a4743f680ad52
Binary files /dev/null and b/icons/obj/weapons/fireaxe.dmi differ
diff --git a/icons/obj/weapons/grenade.dmi b/icons/obj/weapons/grenade.dmi
new file mode 100644
index 0000000000000..415166f9c4074
Binary files /dev/null and b/icons/obj/weapons/grenade.dmi differ
diff --git a/modular_bluemoon/icons/mob/clothing/head/helmet.dmi b/modular_bluemoon/icons/mob/clothing/head/helmet.dmi
new file mode 100644
index 0000000000000..7f91a43759184
Binary files /dev/null and b/modular_bluemoon/icons/mob/clothing/head/helmet.dmi differ
diff --git a/modular_bluemoon/icons/mob/clothing/head/helmet_teshari.dmi b/modular_bluemoon/icons/mob/clothing/head/helmet_teshari.dmi
new file mode 100644
index 0000000000000..04238deebd186
Binary files /dev/null and b/modular_bluemoon/icons/mob/clothing/head/helmet_teshari.dmi differ
diff --git a/modular_bluemoon/icons/mob/clothing/suits/armor.dmi b/modular_bluemoon/icons/mob/clothing/suits/armor.dmi
new file mode 100644
index 0000000000000..1acfbc0fabf28
Binary files /dev/null and b/modular_bluemoon/icons/mob/clothing/suits/armor.dmi differ
diff --git a/modular_bluemoon/icons/mob/clothing/suits/armor_digi.dmi b/modular_bluemoon/icons/mob/clothing/suits/armor_digi.dmi
new file mode 100644
index 0000000000000..7e13766d0415a
Binary files /dev/null and b/modular_bluemoon/icons/mob/clothing/suits/armor_digi.dmi differ
diff --git a/modular_bluemoon/icons/mob/clothing/suits/armor_teshari.dmi b/modular_bluemoon/icons/mob/clothing/suits/armor_teshari.dmi
new file mode 100644
index 0000000000000..35cc34ca2a6e2
Binary files /dev/null and b/modular_bluemoon/icons/mob/clothing/suits/armor_teshari.dmi differ
diff --git a/modular_bluemoon/icons/obj/clothing/head/helmet.dmi b/modular_bluemoon/icons/obj/clothing/head/helmet.dmi
new file mode 100644
index 0000000000000..f45d4b8634f02
Binary files /dev/null and b/modular_bluemoon/icons/obj/clothing/head/helmet.dmi differ
diff --git a/modular_bluemoon/icons/obj/clothing/suits/armor.dmi b/modular_bluemoon/icons/obj/clothing/suits/armor.dmi
new file mode 100644
index 0000000000000..ebff7f2777c76
Binary files /dev/null and b/modular_bluemoon/icons/obj/clothing/suits/armor.dmi differ
diff --git a/sound/announcer/notice/notice3.ogg b/sound/announcer/notice/notice3.ogg
new file mode 100644
index 0000000000000..77ae19af552ca
Binary files /dev/null and b/sound/announcer/notice/notice3.ogg differ
diff --git a/tgstation.dme b/tgstation.dme
index 3a3d844a55f84..3d4492e766ab7 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -776,6 +776,7 @@
#include "code\datums\components\crafting\recipes.dm"
#include "code\datums\components\crafting\glassware\glassware.dm"
#include "code\datums\components\crafting\glassware\lens_crafting.dm"
+#include "code\datums\components\crafting\recipes\recipes_atmospheric_crystals.dm"
#include "code\datums\components\crafting\recipes\recipes_clothing.dm"
#include "code\datums\components\crafting\recipes\recipes_misc.dm"
#include "code\datums\components\crafting\recipes\recipes_primal.dm"
@@ -2203,6 +2204,18 @@
#include "code\modules\atmospherics\machinery\components\binary_devices\relief_valve.dm"
#include "code\modules\atmospherics\machinery\components\binary_devices\valve.dm"
#include "code\modules\atmospherics\machinery\components\binary_devices\volume_pump.dm"
+#include "code\modules\atmospherics\machinery\components\electrolyzer\electrolyzer.dm"
+#include "code\modules\atmospherics\machinery\components\electrolyzer\electrolyzer_reactions.dm"
+#include "code\modules\atmospherics\machinery\components\fusion\_hfr_defines.dm"
+#include "code\modules\atmospherics\machinery\components\fusion\hfr_core.dm"
+#include "code\modules\atmospherics\machinery\components\fusion\hfr_fuel_datums.dm"
+#include "code\modules\atmospherics\machinery\components\fusion\hfr_main_processes.dm"
+#include "code\modules\atmospherics\machinery\components\fusion\hfr_parts.dm"
+#include "code\modules\atmospherics\machinery\components\fusion\hfr_procs.dm"
+#include "code\modules\atmospherics\machinery\components\gas_recipe_machines\crystallizer.dm"
+#include "code\modules\atmospherics\machinery\components\gas_recipe_machines\crystallizer_extra_items.dm"
+#include "code\modules\atmospherics\machinery\components\gas_recipe_machines\crystallizer_items.dm"
+#include "code\modules\atmospherics\machinery\components\gas_recipe_machines\crystallizer_recipes.dm"
#include "code\modules\atmospherics\machinery\components\trinary_devices\filter.dm"
#include "code\modules\atmospherics\machinery\components\trinary_devices\mixer.dm"
#include "code\modules\atmospherics\machinery\components\trinary_devices\trinary_devices.dm"
@@ -4386,9 +4399,9 @@
#include "modular_bluemoon\code\game\objects\items\medals.dm"
#include "modular_bluemoon\code\game\objects\items\miscellaneous.dm"
#include "modular_bluemoon\code\game\objects\items\mop.dm"
-#include "modular_bluemoon\code\game\objects\items\pet_bowl.dm"
#include "modular_bluemoon\code\game\objects\items\noose.dm"
#include "modular_bluemoon\code\game\objects\items\nyamagotchi.dm"
+#include "modular_bluemoon\code\game\objects\items\pet_bowl.dm"
#include "modular_bluemoon\code\game\objects\items\pinpointer.dm"
#include "modular_bluemoon\code\game\objects\items\plushes.dm"
#include "modular_bluemoon\code\game\objects\items\qareen_chalk.dm"
diff --git a/tgui/packages/tgui/interfaces/Hypertorus.js b/tgui/packages/tgui/interfaces/Hypertorus.js
index ab3a196922ac9..b7b17b94cff13 100644
--- a/tgui/packages/tgui/interfaces/Hypertorus.js
+++ b/tgui/packages/tgui/interfaces/Hypertorus.js
@@ -11,7 +11,7 @@ import { Window } from '../layouts';
export const Hypertorus = (props, context) => {
const { act, data } = useBackend(context);
const filterTypes = data.filter_types || [];
- const selectedFuels = data.selected_fuel || [];
+ const selectableFuels = data.selectable_fuel || [];
const {
energy_level,
core_temperature,
@@ -19,7 +19,6 @@ export const Hypertorus = (props, context) => {
power_output,
heat_limiter_modifier,
heat_output,
- heat_output_bool,
heating_conductor,
magnetic_constrictor,
fuel_injection_rate,
@@ -89,14 +88,15 @@ export const Hypertorus = (props, context) => {
-
+
- {selectedFuels.map(recipe => (
+ {selectableFuels.map(recipe => (
0}
- key={recipe.id}
- selected={recipe.id === selected}
+ key={recipe.id ?? 'nothing'}
+ selected={(recipe.id === selected)
+ || (selected === null && recipe.id === null)}
content={recipe.name}
onClick={() => act('fuel', {
mode: recipe.id,
@@ -107,7 +107,7 @@ export const Hypertorus = (props, context) => {
- {product_gases}
+ {product_gases ?? 'None'}
@@ -116,10 +116,10 @@ export const Hypertorus = (props, context) => {
{fusion_gases.map(gas => (
+ key={gas.id}
+ label={getGasLabel(gas.id)}>
@@ -133,10 +133,10 @@ export const Hypertorus = (props, context) => {
{moderator_gases.map(gas => (
+ key={gas.id}
+ label={getGasLabel(gas.id)}>
@@ -199,7 +199,7 @@ export const Hypertorus = (props, context) => {
value={heat_output}
minValue={-1e40}
maxValue={1e30}>
- {heat_output_bool + formatSiBaseTenUnit(heat_output * 1000, 1, 'K')}
+ {(heat_output >= 0 ? '+' : '') + formatSiBaseTenUnit(heat_output * 1000, 1, 'K')}
@@ -321,11 +321,11 @@ export const Hypertorus = (props, context) => {
{filterTypes.map(filter => (
act('filter', {
- mode: filter.id,
+ mode: filter.gas_id,
})} />
))}