diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm
index b9b478fc0b4..b03d49fa14d 100644
--- a/_maps/map_files/Deltastation/DeltaStation2.dmm
+++ b/_maps/map_files/Deltastation/DeltaStation2.dmm
@@ -13680,7 +13680,6 @@
/obj/effect/turf_decal/tile/neutral{
dir = 4
},
-/obj/machinery/atmospherics/pipe/smart/manifold4w/general/visible,
/obj/machinery/airalarm/directional/east,
/obj/structure/cable,
/turf/open/floor/iron,
@@ -16441,7 +16440,6 @@
/obj/effect/turf_decal/tile/neutral{
dir = 4
},
-/obj/machinery/atmospherics/pipe/smart/manifold4w/general/visible,
/turf/open/floor/iron,
/area/science/mixing/launch)
"cKP" = (
@@ -65648,7 +65646,7 @@
/turf/open/floor/iron/white/side,
/area/service/kitchen/abandoned)
"mHA" = (
-/obj/machinery/research/explosive_compressor,
+/obj/machinery/research/anomaly_refinery,
/obj/effect/turf_decal/bot,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
/turf/open/floor/iron,
@@ -78578,7 +78576,9 @@
/area/ai_monitored/command/storage/eva)
"pUU" = (
/obj/effect/turf_decal/stripes/line,
-/obj/machinery/atmospherics/components/unary/portables_connector/visible,
+/obj/machinery/atmospherics/components/binary/tank_compressor{
+ dir = 4
+ },
/turf/open/floor/iron,
/area/science/mixing/launch)
"pVL" = (
@@ -81687,11 +81687,23 @@
"qOX" = (
/obj/structure/table,
/obj/effect/decal/cleanable/dirt,
-/obj/item/clothing/gloves/color/latex,
-/obj/item/clothing/glasses/science,
-/obj/item/clothing/glasses/science,
/obj/effect/turf_decal/bot,
/obj/machinery/light/directional/south,
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -5;
+ pixel_y = 8
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = 7;
+ pixel_y = 2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -8;
+ pixel_y = -3
+ },
/turf/open/floor/iron,
/area/science/mixing/launch)
"qPa" = (
@@ -102359,11 +102371,13 @@
/turf/open/floor/wood,
/area/commons/dorms)
"vZS" = (
+/obj/machinery/newscaster/directional/north,
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 8
+ },
/obj/effect/turf_decal/stripes/line{
dir = 6
},
-/obj/machinery/atmospherics/components/unary/portables_connector/visible,
-/obj/machinery/newscaster/directional/north,
/obj/machinery/light_switch/directional/east{
pixel_y = 26
},
@@ -104585,7 +104599,7 @@
/turf/open/floor/iron,
/area/science/lobby)
"wCz" = (
-/obj/machinery/doppler_array/research/science{
+/obj/machinery/doppler_array{
dir = 8
},
/obj/effect/turf_decal/stripes/line{
@@ -104651,9 +104665,6 @@
/obj/effect/turf_decal/tile/neutral{
dir = 4
},
-/obj/machinery/atmospherics/components/trinary/filter{
- dir = 8
- },
/turf/open/floor/iron,
/area/science/mixing/launch)
"wCU" = (
@@ -108567,7 +108578,9 @@
/obj/effect/turf_decal/stripes/line{
dir = 10
},
-/obj/machinery/atmospherics/components/unary/portables_connector/visible,
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 4
+ },
/turf/open/floor/iron,
/area/science/mixing/launch)
"xHF" = (
diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
index a66093e49a3..7268f54f23a 100644
--- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm
+++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
@@ -5496,9 +5496,9 @@
/turf/open/floor/iron,
/area/hallway/primary/central)
"aXj" = (
-/obj/machinery/portable_atmospherics/canister/oxygen,
/obj/effect/turf_decal/bot,
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/portable_atmospherics/canister/plasma,
/turf/open/floor/iron/dark/textured_large,
/area/science/storage)
"aXp" = (
@@ -9392,6 +9392,7 @@
/obj/structure/sign/warning/fire{
pixel_x = 32
},
+/obj/machinery/portable_atmospherics/canister,
/turf/open/floor/iron,
/area/science/mixing)
"cjS" = (
@@ -16472,9 +16473,9 @@
/turf/open/floor/iron,
/area/cargo/sorting)
"fvo" = (
-/obj/machinery/portable_atmospherics/canister/tier_1,
/obj/effect/turf_decal/bot,
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/portable_atmospherics/canister/nitrogen,
/turf/open/floor/iron/dark/textured_large,
/area/science/storage)
"fvu" = (
@@ -17656,10 +17657,10 @@
/turf/open/floor/iron/freezer,
/area/commons/toilet/locker)
"fWL" = (
-/obj/machinery/portable_atmospherics/canister/oxygen,
/obj/effect/turf_decal/bot,
/obj/effect/decal/cleanable/dirt,
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/portable_atmospherics/canister/plasma,
/turf/open/floor/iron/dark/textured_large,
/area/science/storage)
"fWQ" = (
@@ -20356,9 +20357,23 @@
network = list("ss13","rd")
},
/obj/structure/table,
-/obj/item/clothing/glasses/science,
/obj/structure/cable,
/obj/machinery/power/apc/auto_name/directional/north,
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -5;
+ pixel_y = 8
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = 7;
+ pixel_y = 2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -8;
+ pixel_y = -3
+ },
/turf/open/floor/iron,
/area/science/mixing/launch)
"hgT" = (
@@ -21249,10 +21264,10 @@
/turf/closed/wall,
/area/engineering/atmos)
"hFn" = (
-/obj/machinery/atmospherics/components/trinary/filter/flipped{
+/obj/machinery/light/directional/north,
+/obj/machinery/atmospherics/components/binary/tank_compressor{
dir = 4
},
-/obj/machinery/light/directional/north,
/turf/open/floor/iron,
/area/science/mixing)
"hFq" = (
@@ -23841,10 +23856,10 @@
/turf/open/floor/iron/white,
/area/science/mixing)
"iSF" = (
-/obj/machinery/portable_atmospherics/canister/plasma,
/obj/effect/turf_decal/bot,
/obj/structure/extinguisher_cabinet/directional/west,
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/portable_atmospherics/canister/oxygen,
/turf/open/floor/iron/dark/textured_large,
/area/science/storage)
"iSQ" = (
@@ -25615,6 +25630,7 @@
dir = 8
},
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/portable_atmospherics/canister/nitrogen,
/turf/open/floor/iron,
/area/science/mixing)
"jKR" = (
@@ -27510,10 +27526,10 @@
/turf/open/floor/iron/dark,
/area/command/heads_quarters/cmo)
"kIF" = (
-/obj/machinery/portable_atmospherics/canister/oxygen,
/obj/effect/turf_decal/bot,
/obj/machinery/firealarm/directional/west,
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/portable_atmospherics/canister/plasma,
/turf/open/floor/iron/dark/textured_large,
/area/science/storage)
"kIT" = (
@@ -27631,7 +27647,6 @@
/turf/open/floor/iron/white,
/area/science/mixing)
"kMd" = (
-/obj/machinery/portable_atmospherics/canister/tier_1,
/obj/effect/turf_decal/stripes/line,
/turf/open/floor/iron,
/area/science/mixing)
@@ -29173,6 +29188,7 @@
dir = 10
},
/obj/effect/turf_decal/bot,
+/obj/machinery/portable_atmospherics/canister,
/turf/open/floor/iron,
/area/science/mixing)
"lFx" = (
@@ -41807,7 +41823,7 @@
/turf/open/floor/glass/reinforced,
/area/engineering/atmos)
"rXL" = (
-/obj/machinery/research/explosive_compressor,
+/obj/machinery/research/anomaly_refinery,
/obj/effect/turf_decal/bot,
/obj/item/radio/intercom/directional/north,
/turf/open/floor/iron,
@@ -45149,10 +45165,10 @@
/turf/open/floor/iron,
/area/command/teleporter)
"tDW" = (
-/obj/machinery/portable_atmospherics/canister/plasma,
/obj/effect/turf_decal/bot,
/obj/machinery/light/directional/south,
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/portable_atmospherics/canister/oxygen,
/turf/open/floor/iron/dark/textured_large,
/area/science/storage)
"tEd" = (
@@ -45169,8 +45185,8 @@
"tEi" = (
/obj/effect/turf_decal/bot,
/obj/effect/decal/cleanable/dirt,
-/obj/machinery/portable_atmospherics/canister/tier_1,
/obj/structure/extinguisher_cabinet/directional/east,
+/obj/machinery/portable_atmospherics/canister/nitrogen,
/turf/open/floor/iron/dark/textured_large,
/area/science/storage)
"tEm" = (
@@ -50789,9 +50805,9 @@
/turf/closed/wall/r_wall,
/area/command/gateway)
"wnu" = (
-/obj/machinery/portable_atmospherics/canister/plasma,
/obj/effect/turf_decal/bot,
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/portable_atmospherics/canister/oxygen,
/turf/open/floor/iron/dark/textured_large,
/area/science/storage)
"wnP" = (
@@ -50844,13 +50860,13 @@
},
/area/service/chapel)
"woB" = (
-/obj/machinery/portable_atmospherics/canister/plasma,
/obj/effect/turf_decal/bot,
/obj/machinery/camera/autoname/directional/south{
c_tag = "Gas Storage";
network = list("ss13","rd")
},
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/portable_atmospherics/canister/oxygen,
/turf/open/floor/iron/dark/textured_large,
/area/science/storage)
"woD" = (
@@ -50969,7 +50985,7 @@
/turf/open/floor/iron,
/area/hallway/primary/central)
"wrl" = (
-/obj/machinery/doppler_array/research/science{
+/obj/machinery/doppler_array{
dir = 4
},
/obj/effect/turf_decal/bot,
@@ -104372,7 +104388,7 @@ tKu
kMq
bEC
hFn
-mpU
+kMd
iqv
jJE
bEs
diff --git a/_maps/map_files/KiloStation/KiloStation.dmm b/_maps/map_files/KiloStation/KiloStation.dmm
index 0c9e1f90acf..a9e42fecf51 100644
--- a/_maps/map_files/KiloStation/KiloStation.dmm
+++ b/_maps/map_files/KiloStation/KiloStation.dmm
@@ -3539,6 +3539,22 @@
/obj/machinery/power/apc/auto_name/directional/north,
/obj/structure/cable,
/obj/effect/decal/cleanable/dirt,
+/obj/structure/table,
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -5;
+ pixel_y = 8
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = 7;
+ pixel_y = 2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -8;
+ pixel_y = -3
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -2
+ },
/turf/open/floor/iron/dark,
/area/science/mixing/launch)
"aqD" = (
@@ -6123,9 +6139,6 @@
/turf/open/floor/plating,
/area/hallway/primary/fore)
"aEG" = (
-/obj/machinery/atmospherics/components/unary/portables_connector/visible{
- dir = 1
- },
/obj/effect/turf_decal/tile/neutral{
dir = 4
},
@@ -10822,13 +10835,13 @@
/turf/open/floor/iron/dark,
/area/science/robotics/lab)
"bbS" = (
-/obj/machinery/atmospherics/components/trinary/mixer/flipped{
- dir = 1
- },
/obj/effect/turf_decal/tile/neutral{
dir = 4
},
/obj/effect/turf_decal/tile/neutral,
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 4
+ },
/turf/open/floor/iron/dark,
/area/science/mixing)
"bbU" = (
@@ -10917,6 +10930,7 @@
dir = 1
},
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/portable_atmospherics/canister,
/turf/open/floor/iron/dark,
/area/science/mixing/chamber)
"bch" = (
@@ -11081,6 +11095,7 @@
dir = 8
},
/obj/effect/decal/cleanable/dirt,
+/obj/machinery/atmospherics/components/unary/portables_connector/visible,
/turf/open/floor/iron/dark,
/area/science/mixing)
"bcK" = (
@@ -15447,6 +15462,7 @@
dir = 4;
pixel_x = 25
},
+/obj/machinery/portable_atmospherics/canister,
/turf/open/floor/iron/dark,
/area/science/mixing/chamber)
"bHI" = (
@@ -26868,8 +26884,8 @@
/obj/effect/turf_decal/tile/neutral{
dir = 1
},
-/obj/machinery/computer/atmos_control/ordnancemix{
- dir = 8
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 1
},
/turf/open/floor/iron/dark,
/area/science/mixing)
@@ -32585,13 +32601,13 @@
/turf/open/floor/iron/dark,
/area/command/heads_quarters/ce)
"fyH" = (
-/obj/machinery/portable_atmospherics/scrubber/huge,
/obj/effect/turf_decal/delivery,
/obj/effect/turf_decal/tile/neutral,
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
/obj/structure/extinguisher_cabinet/directional/east,
+/obj/machinery/portable_atmospherics/canister/nitrogen,
/turf/open/floor/iron/dark,
/area/science/storage)
"fyV" = (
@@ -44127,14 +44143,12 @@
},
/area/maintenance/fore)
"jLK" = (
-/obj/machinery/doppler_array/research/science{
- dir = 4
- },
/obj/item/radio/intercom/directional/east,
/obj/effect/turf_decal/delivery,
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/machinery/research/anomaly_refinery,
/turf/open/floor/iron/dark,
/area/science/mixing/launch)
"jLU" = (
@@ -51322,17 +51336,6 @@
/turf/open/floor/engine/vacuum,
/area/engineering/atmos)
"mjh" = (
-/obj/structure/chair{
- dir = 4
- },
-/obj/machinery/computer/security/telescreen{
- desc = "Used for watching the test chamber.";
- dir = 8;
- layer = 4;
- name = "Test Chamber Telescreen";
- network = list("ordnance");
- pixel_x = 30
- },
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
@@ -51343,6 +51346,10 @@
/obj/machinery/computer/pod/old/mass_driver_controller/ordnancedriver/longrange{
pixel_y = -26
},
+/obj/machinery/doppler_array{
+ dir = 4
+ },
+/obj/effect/turf_decal/delivery,
/turf/open/floor/iron/dark,
/area/science/mixing/launch)
"mjr" = (
@@ -59354,7 +59361,6 @@
/obj/effect/turf_decal/tile/purple{
dir = 8
},
-/obj/machinery/research/explosive_compressor,
/turf/open/floor/iron/showroomfloor,
/area/science/mixing/launch)
"pgJ" = (
@@ -67425,9 +67431,6 @@
/turf/open/floor/iron/dark,
/area/service/library)
"rZg" = (
-/obj/machinery/atmospherics/components/unary/portables_connector/visible{
- dir = 8
- },
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
@@ -67439,6 +67442,7 @@
name = "science camera";
network = list("ss13","rd")
},
+/obj/machinery/atmospherics/components/trinary/filter/flipped,
/turf/open/floor/iron/dark,
/area/science/mixing)
"rZr" = (
@@ -70713,13 +70717,14 @@
/turf/open/floor/engine,
/area/ai_monitored/turret_protected/ai)
"tlV" = (
-/obj/machinery/atmospherics/components/unary/portables_connector/visible,
/obj/effect/turf_decal/box/corners{
dir = 1
},
/obj/effect/turf_decal/tile/neutral,
/obj/item/radio/intercom/directional/north,
-/obj/machinery/portable_atmospherics/canister,
+/obj/machinery/portable_atmospherics/scrubber{
+ name = "scrubber ducky"
+ },
/turf/open/floor/iron/dark,
/area/science/mixing)
"tmg" = (
@@ -77171,6 +77176,9 @@
dir = 1
},
/obj/structure/extinguisher_cabinet/directional/east,
+/obj/machinery/computer/atmos_control/ordnancemix{
+ dir = 8
+ },
/turf/open/floor/iron/dark,
/area/science/mixing)
"vHK" = (
@@ -81746,6 +81754,7 @@
},
/obj/structure/cable,
/obj/machinery/power/apc/auto_name/directional/east,
+/obj/machinery/atmospherics/components/binary/tank_compressor,
/turf/open/floor/iron/dark,
/area/science/mixing)
"xrO" = (
@@ -83601,11 +83610,9 @@
/obj/effect/turf_decal/tile/neutral{
dir = 1
},
-/obj/machinery/portable_atmospherics/scrubber{
- name = "scrubber ducky"
- },
/obj/structure/cable,
/obj/machinery/power/apc/auto_name/directional/north,
+/obj/machinery/portable_atmospherics/canister/nitrogen,
/turf/open/floor/iron/dark,
/area/science/storage)
"xZv" = (
diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm
index 8ea929f69c0..6c7c478b053 100644
--- a/_maps/map_files/MetaStation/MetaStation.dmm
+++ b/_maps/map_files/MetaStation/MetaStation.dmm
@@ -5168,6 +5168,7 @@
/obj/machinery/atmospherics/components/unary/portables_connector/visible,
/obj/effect/turf_decal/stripes/line,
/obj/machinery/light/small/directional/west,
+/obj/machinery/portable_atmospherics/canister,
/turf/open/floor/iron,
/area/science/mixing)
"aWK" = (
@@ -14158,8 +14159,8 @@
/obj/effect/turf_decal/stripes/white/line{
dir = 1
},
-/obj/machinery/holopad,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
+/obj/machinery/atmospherics/components/unary/portables_connector/visible,
/turf/open/floor/iron,
/area/science/mixing)
"djf" = (
@@ -15909,8 +15910,23 @@
/turf/open/floor/iron/dark,
/area/engineering/atmos)
"dMq" = (
-/obj/structure/filingcabinet/chestdrawer,
/obj/machinery/light_switch/directional/west,
+/obj/structure/table,
+/obj/item/transfer_valve{
+ pixel_x = 5
+ },
+/obj/item/transfer_valve{
+ pixel_x = 5
+ },
+/obj/item/transfer_valve{
+ pixel_x = 5
+ },
+/obj/item/transfer_valve{
+ pixel_x = 5
+ },
+/obj/item/transfer_valve{
+ pixel_x = 5
+ },
/turf/open/floor/iron/white,
/area/science/mixing/launch)
"dMw" = (
@@ -17910,7 +17926,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
-/obj/machinery/research/explosive_compressor,
+/obj/machinery/research/anomaly_refinery,
/obj/effect/turf_decal/delivery,
/turf/open/floor/iron/dark,
/area/science/mixing/launch)
@@ -20790,7 +20806,19 @@
},
/obj/structure/table,
/obj/effect/turf_decal/bot,
-/obj/item/clothing/mask/gas,
+/obj/item/assembly/timer{
+ pixel_x = -4;
+ pixel_y = 2
+ },
+/obj/item/assembly/timer{
+ pixel_x = 5;
+ pixel_y = 4
+ },
+/obj/item/assembly/timer{
+ pixel_x = 6;
+ pixel_y = -4
+ },
+/obj/item/assembly/timer,
/turf/open/floor/iron/dark,
/area/science/mixing/launch)
"foF" = (
@@ -24074,6 +24102,11 @@
},
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
+/obj/structure/closet/crate/secure{
+ desc = "A secure crate containing various materials for building a customised test-site.";
+ name = "Test Site Materials Crate";
+ req_access_txt = "8"
+ },
/turf/open/floor/iron/dark,
/area/science/mixing/launch)
"grN" = (
@@ -27309,7 +27342,7 @@
dir = 8
},
/obj/machinery/light/directional/south,
-/obj/machinery/doppler_array/research/science{
+/obj/machinery/doppler_array{
dir = 4
},
/obj/effect/turf_decal/delivery,
@@ -30732,14 +30765,11 @@
/turf/open/floor/wood,
/area/service/theater)
"iNw" = (
-/obj/structure/table/reinforced,
-/obj/item/wrench,
-/obj/item/screwdriver{
- pixel_y = 10
+/obj/machinery/firealarm/directional/north,
+/obj/machinery/atmospherics/components/binary/tank_compressor{
+ dir = 8
},
/obj/effect/decal/cleanable/dirt,
-/obj/item/analyzer,
-/obj/item/pipe_dispenser,
/obj/effect/turf_decal/bot,
/obj/machinery/firealarm/directional/north,
/obj/structure/cable,
@@ -31266,7 +31296,9 @@
/turf/open/floor/iron,
/area/commons/fitness/recreation)
"iXv" = (
-/obj/machinery/atmospherics/components/unary/portables_connector/visible,
+/obj/machinery/atmospherics/components/trinary/mixer{
+ dir = 1
+ },
/turf/open/floor/iron,
/area/science/mixing)
"iXC" = (
@@ -31599,9 +31631,22 @@
},
/obj/item/radio/intercom/directional/east,
/obj/structure/table,
-/obj/item/clothing/gloves/color/latex,
-/obj/item/clothing/glasses/science,
/obj/effect/turf_decal/bot,
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = 7;
+ pixel_y = 2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -5;
+ pixel_y = 8
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -8;
+ pixel_y = -3
+ },
/turf/open/floor/iron/dark,
/area/science/mixing/launch)
"jfl" = (
@@ -34183,6 +34228,7 @@
/area/maintenance/aft/greater)
"kbu" = (
/obj/structure/cable,
+/obj/machinery/atmospherics/components/trinary/filter/flipped,
/turf/open/floor/iron,
/area/science/mixing)
"kbv" = (
@@ -37936,7 +37982,8 @@
/turf/open/floor/iron,
/area/hallway/primary/starboard)
"lrs" = (
-/obj/structure/filingcabinet/chestdrawer,
+/obj/structure/table,
+/obj/item/storage/toolbox/mechanical,
/turf/open/floor/iron/white,
/area/science/mixing/launch)
"lry" = (
@@ -44526,7 +44573,8 @@
"nyc" = (
/obj/structure/table,
/obj/effect/turf_decal/bot,
-/obj/item/clothing/mask/gas,
+/obj/item/storage/toolbox/mechanical,
+/obj/item/pipe_dispenser,
/turf/open/floor/iron/white,
/area/science/mixing)
"nyf" = (
@@ -45521,6 +45569,10 @@
},
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
+/obj/item/target/alien,
+/obj/item/target/alien,
+/obj/item/target/clown,
+/obj/item/target/clown,
/turf/open/floor/iron/dark,
/area/science/mixing/launch)
"nPM" = (
@@ -45808,6 +45860,12 @@
/obj/machinery/duct,
/turf/open/floor/iron/white,
/area/medical/treatment_center)
+"nUP" = (
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 8
+ },
+/turf/open/floor/iron,
+/area/science/mixing)
"nUR" = (
/obj/effect/turf_decal/tile/blue{
dir = 1
@@ -46219,6 +46277,10 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
/turf/open/floor/plating,
/area/maintenance/department/science/xenobiology)
+"obt" = (
+/obj/effect/landmark/start/scientist,
+/turf/open/floor/iron/white,
+/area/science/mixing/launch)
"obv" = (
/obj/structure/sign/poster/contraband/random{
pixel_x = 32
@@ -46864,25 +46926,12 @@
/turf/open/floor/iron/dark,
/area/engineering/supermatter/room)
"omN" = (
-/obj/item/assembly/signaler{
- pixel_y = 8
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 8
},
-/obj/item/assembly/signaler{
- pixel_x = -8;
- pixel_y = 5
- },
-/obj/item/assembly/signaler{
- pixel_x = 6;
- pixel_y = 5
- },
-/obj/item/assembly/signaler{
- pixel_x = -2;
- pixel_y = -2
- },
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/bot,
/obj/structure/cable,
/obj/machinery/power/apc/auto_name/directional/north,
+/obj/effect/turf_decal/box,
/turf/open/floor/iron/white,
/area/science/mixing/launch)
"omY" = (
@@ -47024,24 +47073,13 @@
},
/area/service/chapel/funeral)
"opb" = (
-/obj/structure/table/reinforced,
-/obj/item/transfer_valve{
- pixel_x = 5
+/obj/effect/turf_decal/stripes/white/line{
+ dir = 1
},
-/obj/item/transfer_valve{
- pixel_x = 5
- },
-/obj/item/transfer_valve{
- pixel_x = 5
- },
-/obj/item/transfer_valve{
- pixel_x = 5
- },
-/obj/item/transfer_valve{
- pixel_x = 5
- },
-/turf/open/floor/iron/white,
-/area/science/mixing/launch)
+/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
+/obj/machinery/holopad,
+/turf/open/floor/iron,
+/area/science/mixing)
"opc" = (
/obj/effect/turf_decal/stripes/line{
dir = 10
@@ -52398,9 +52436,7 @@
"qjh" = (
/obj/machinery/power/apc/auto_name/directional/east,
/obj/structure/cable,
-/obj/machinery/atmospherics/components/unary/portables_connector/visible{
- dir = 8
- },
+/obj/machinery/atmospherics/components/unary/portables_connector/visible,
/turf/open/floor/iron,
/area/science/mixing)
"qjo" = (
@@ -52628,7 +52664,7 @@
/turf/open/floor/iron,
/area/engineering/atmos)
"qmZ" = (
-/obj/machinery/atmospherics/components/trinary/filter{
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
dir = 1
},
/turf/open/floor/iron,
@@ -53926,6 +53962,9 @@
"qKO" = (
/obj/machinery/status_display/evac/directional/south,
/obj/structure/cable,
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 1
+ },
/turf/open/floor/iron,
/area/science/mixing)
"qLc" = (
@@ -57237,16 +57276,10 @@
/turf/open/floor/iron,
/area/construction/storage_wing)
"rVt" = (
-/obj/effect/turf_decal/bot,
-/obj/structure/closet/crate/secure{
- desc = "A secure crate containing various materials for building a customised test-site.";
- name = "Test Site Materials Crate";
- req_access_txt = "8"
+/obj/effect/turf_decal/box,
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 4
},
-/obj/item/target/clown,
-/obj/item/target/clown,
-/obj/item/target/alien,
-/obj/item/target/alien,
/obj/structure/cable,
/turf/open/floor/iron/white,
/area/science/mixing/launch)
@@ -60060,6 +60093,7 @@
/area/maintenance/fore/lesser)
"sXG" = (
/obj/structure/disposalpipe/segment,
+/obj/machinery/holopad,
/turf/open/floor/iron/white,
/area/science/mixing/launch)
"sXI" = (
@@ -60587,7 +60621,7 @@
/area/hallway/secondary/entry)
"tgJ" = (
/obj/machinery/atmospherics/components/unary/portables_connector/visible{
- dir = 1
+ dir = 4
},
/turf/open/floor/iron,
/area/science/mixing)
@@ -61426,11 +61460,23 @@
"ttZ" = (
/obj/machinery/airalarm/directional/west,
/obj/structure/table,
-/obj/item/crowbar,
-/obj/item/wrench,
-/obj/item/clothing/mask/gas,
/obj/machinery/power/apc/auto_name/directional/south,
/obj/structure/cable,
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = 7;
+ pixel_y = 2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -5;
+ pixel_y = 8
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -8;
+ pixel_y = -3
+ },
/turf/open/floor/iron/white,
/area/science/storage)
"tuk" = (
@@ -64299,10 +64345,24 @@
},
/obj/structure/table,
/obj/effect/turf_decal/bot,
-/obj/item/wrench,
/obj/machinery/camera/directional/west{
c_tag = "Science Ordnance Test Lab"
},
+/obj/item/assembly/prox_sensor{
+ pixel_y = 2
+ },
+/obj/item/assembly/prox_sensor{
+ pixel_x = 9;
+ pixel_y = -2
+ },
+/obj/item/assembly/prox_sensor{
+ pixel_x = -4;
+ pixel_y = 1
+ },
+/obj/item/assembly/prox_sensor{
+ pixel_x = 8;
+ pixel_y = 9
+ },
/turf/open/floor/iron/dark,
/area/science/mixing/launch)
"uxp" = (
@@ -64515,22 +64575,8 @@
/turf/open/floor/plating,
/area/service/hydroponics/garden)
"uCp" = (
-/obj/item/assembly/timer{
- pixel_x = 5;
- pixel_y = 4
- },
-/obj/item/assembly/timer{
- pixel_x = -4;
- pixel_y = 2
- },
-/obj/item/assembly/timer{
- pixel_x = 6;
- pixel_y = -4
- },
-/obj/item/assembly/timer,
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/bot,
/obj/item/radio/intercom/directional/east,
+/obj/structure/tank_dispenser,
/turf/open/floor/iron/white,
/area/science/mixing/launch)
"uCA" = (
@@ -67461,9 +67507,6 @@
/turf/open/floor/iron/dark/telecomms,
/area/tcommsat/server)
"vEn" = (
-/obj/structure/chair/office/light{
- dir = 8
- },
/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2,
/turf/open/floor/iron/white,
/area/science/mixing/launch)
@@ -72856,12 +72899,26 @@
/obj/effect/turf_decal/stripes/white/line{
dir = 1
},
-/obj/item/crowbar,
/obj/machinery/requests_console/directional/west{
department = "Ordnance Test Range";
departmentType = 5;
name = "Test Range Requests Console"
},
+/obj/item/assembly/signaler{
+ pixel_x = 6;
+ pixel_y = 5
+ },
+/obj/item/assembly/signaler{
+ pixel_x = -2;
+ pixel_y = -2
+ },
+/obj/item/assembly/signaler{
+ pixel_x = -8;
+ pixel_y = 5
+ },
+/obj/item/assembly/signaler{
+ pixel_y = 8
+ },
/turf/open/floor/iron/dark,
/area/science/mixing/launch)
"xyl" = (
@@ -73333,8 +73390,10 @@
/turf/open/floor/iron,
/area/hallway/secondary/command)
"xGk" = (
-/obj/machinery/holopad,
-/obj/effect/landmark/start/scientist,
+/obj/effect/turf_decal/box,
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 1
+ },
/turf/open/floor/iron/white,
/area/science/mixing/launch)
"xGy" = (
@@ -74608,25 +74667,13 @@
/turf/open/floor/plating,
/area/maintenance/fore)
"yhY" = (
-/obj/item/assembly/prox_sensor{
- pixel_x = -4;
- pixel_y = 1
+/obj/machinery/light/directional/north,
+/obj/machinery/atmospherics/components/trinary/filter{
+ dir = 4
},
-/obj/item/assembly/prox_sensor{
- pixel_x = 8;
- pixel_y = 9
- },
-/obj/item/assembly/prox_sensor{
- pixel_x = 9;
- pixel_y = -2
- },
-/obj/item/assembly/prox_sensor{
- pixel_y = 2
- },
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/bot,
/obj/machinery/light/directional/north,
/obj/structure/cable,
+/obj/effect/turf_decal/bot,
/turf/open/floor/iron/white,
/area/science/mixing/launch)
"yib" = (
@@ -105728,8 +105775,8 @@ jvq
uGO
gmC
eqN
-swS
-nrI
+opb
+nUP
qjh
kbu
qKO
@@ -109587,7 +109634,7 @@ ged
ged
crR
rVt
-jnU
+obt
jFj
iux
jfb
@@ -110615,7 +110662,7 @@ dvY
dvY
nOc
uCp
-opb
+jnU
eID
vLD
ylf
diff --git a/_maps/map_files/tramstation/tramstation.dmm b/_maps/map_files/tramstation/tramstation.dmm
index 6c9b7834e37..b7a929b3a5b 100644
--- a/_maps/map_files/tramstation/tramstation.dmm
+++ b/_maps/map_files/tramstation/tramstation.dmm
@@ -9,6 +9,7 @@
"aae" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/obj/structure/cable,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
/turf/open/floor/iron,
/area/science/mixing/launch)
"aas" = (
@@ -14367,6 +14368,7 @@
/area/security/processing)
"dIi" = (
/obj/structure/cable,
+/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2,
/turf/open/floor/iron,
/area/science/mixing/launch)
"dIv" = (
@@ -21068,8 +21070,8 @@
/turf/open/floor/iron,
/area/commons/storage/art)
"fZU" = (
-/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2,
/obj/machinery/firealarm/directional/west,
+/obj/machinery/research/anomaly_refinery,
/turf/open/floor/iron,
/area/science/mixing/launch)
"gal" = (
@@ -23050,12 +23052,9 @@
/turf/open/floor/iron/dark,
/area/medical/morgue)
"gJk" = (
-/obj/machinery/research/explosive_compressor,
/obj/effect/turf_decal/bot,
-/obj/machinery/camera{
- c_tag = "Science - Ordnance Launch Room";
- dir = 9;
- network = list("ss13","rd")
+/obj/machinery/atmospherics/components/binary/tank_compressor{
+ dir = 8
},
/turf/open/floor/iron,
/area/science/mixing/launch)
@@ -34055,6 +34054,14 @@
"kNV" = (
/obj/effect/turf_decal/bot,
/obj/machinery/airalarm/directional/north,
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 8
+ },
+/obj/machinery/camera{
+ c_tag = "Science - Ordnance Launch Room";
+ dir = 9;
+ network = list("ss13","rd")
+ },
/turf/open/floor/iron,
/area/science/mixing/launch)
"kOa" = (
@@ -43615,12 +43622,25 @@
/turf/open/floor/iron,
/area/engineering/main)
"opt" = (
-/obj/structure/chair{
- dir = 4
- },
/obj/effect/turf_decal/stripes/line{
dir = 6
},
+/obj/structure/table,
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -5;
+ pixel_y = 8
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = 7;
+ pixel_y = 2
+ },
+/obj/item/computer_hardware/hard_drive/portable{
+ pixel_x = -8;
+ pixel_y = -3
+ },
/turf/open/floor/iron,
/area/science/mixing/launch)
"opw" = (
@@ -50714,6 +50734,9 @@
/turf/open/floor/catwalk_floor,
/area/hallway/primary/tram/left)
"qUg" = (
+/obj/machinery/atmospherics/components/unary/portables_connector/visible{
+ dir = 4
+ },
/obj/effect/turf_decal/bot,
/obj/structure/cable,
/obj/machinery/power/apc/auto_name/directional/north,
@@ -51925,7 +51948,7 @@
/obj/effect/turf_decal/stripes/line{
dir = 5
},
-/obj/machinery/doppler_array/research/science{
+/obj/machinery/doppler_array{
dir = 4
},
/obj/item/radio/intercom/directional/north,
diff --git a/code/__DEFINES/atmospherics/atmos_piping.dm b/code/__DEFINES/atmospherics/atmos_piping.dm
index 98a74102c18..f0919f37276 100644
--- a/code/__DEFINES/atmospherics/atmos_piping.dm
+++ b/code/__DEFINES/atmospherics/atmos_piping.dm
@@ -51,6 +51,13 @@
#define TANK_FRAGMENT_PRESSURE (40.*ONE_ATMOSPHERE)
/// Range scaling constant for tank explosions. Calibrated so that a TTV assembled using two 70L tanks will hit the maxcap at at least 160atm.
#define TANK_FRAGMENT_SCALE (84.*ONE_ATMOSPHERE)
+/// Denotes that our tank is overpressurized simply from gas merging.
+#define TANK_MERGE_OVERPRESSURE "tank_overpressure"
+// Indices for the reaction_results returned by explosion_information()
+/// Reactions that have happened in the tank.
+#define TANK_RESULTS_REACTION 1
+/// Additional information of the tank.
+#define TANK_RESULTS_MISC 2
//MULTIPIPES
//IF YOU EVER CHANGE THESE CHANGE SPRITES TO MATCH.
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_explosion.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_explosion.dm
index a2caf6b2095..d2328f5e500 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_explosion.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_explosion.dm
@@ -10,5 +10,4 @@
#define COMSIG_AREA_INTERNAL_EXPLOSION "area_internal_explosion"
/// When returned on a signal hooked to [COMSIG_ATOM_EXPLODE], [COMSIG_ATOM_INTERNAL_EXPLOSION], or [COMSIG_AREA_INTERNAL_EXPLOSION] it prevents the explosion from being propagated further.
#define COMSIG_CANCEL_EXPLOSION (1<<0)
-///from /obj/machinery/doppler_array/proc/sense_explosion(...): Runs when an explosion is succesfully detected by a doppler array(turf/epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range)
-#define COMSIG_DOPPLER_ARRAY_EXPLOSION_DETECTED "atom_dopplerarray_explosion_detected"
+
diff --git a/code/__DEFINES/dcs/signals/signals_datum.dm b/code/__DEFINES/dcs/signals/signals_datum.dm
index 1a57698dd64..826dd0e6642 100644
--- a/code/__DEFINES/dcs/signals/signals_datum.dm
+++ b/code/__DEFINES/dcs/signals/signals_datum.dm
@@ -31,3 +31,21 @@
#define COMSIG_MERGER_REMOVING "comsig_merger_removing"
/// Called on the merger after finishing a refresh: (list/leaving_members, list/joining_members)
#define COMSIG_MERGER_REFRESH_COMPLETE "comsig_merger_refresh_complete"
+
+// Gas mixture signals
+/// From /datum/gas_mixture/proc/merge: ()
+#define COMSIG_GASMIX_MERGED "comsig_gasmix_merged"
+/// From /datum/gas_mixture/proc/remove: ()
+#define COMSIG_GASMIX_REMOVED "comsig_gasmix_removed"
+/// From /datum/gas_mixture/proc/react: ()
+#define COMSIG_GASMIX_REACTED "comsig_gasmix_reacted"
+
+// Modular computer's file signals. Tells the program datum something is going on.
+/// From /obj/item/computer_hardware/hard_drive/proc/store_file: ()
+#define COMSIG_MODULAR_COMPUTER_FILE_ADDING "comsig_modular_computer_file_adding"
+/// From /obj/item/computer_hardware/hard_drive/proc/store_file: ()
+#define COMSIG_MODULAR_COMPUTER_FILE_ADDED "comsig_modular_computer_file_adding"
+/// From /obj/item/computer_hardware/hard_drive/proc/remove_file: ()
+#define COMSIG_MODULAR_COMPUTER_FILE_DELETING "comsig_modular_computer_file_deleting"
+/// From /obj/item/computer_hardware/hard_drive/proc/store_file: ()
+#define COMSIG_MODULAR_COMPUTER_FILE_DELETED "comsig_modular_computer_file_adding"
diff --git a/code/__DEFINES/machines.dm b/code/__DEFINES/machines.dm
index 569fe5a0629..249c178b52d 100644
--- a/code/__DEFINES/machines.dm
+++ b/code/__DEFINES/machines.dm
@@ -98,8 +98,8 @@
//Program categories
#define PROGRAM_CATEGORY_CREW "Crew"
#define PROGRAM_CATEGORY_ENGI "Engineering"
-#define PROGRAM_CATEGORY_ROBO "Robotics"
#define PROGRAM_CATEGORY_SUPL "Supply"
+#define PROGRAM_CATEGORY_SCI "Science"
#define PROGRAM_CATEGORY_MISC "Other"
#define FIREDOOR_OPEN 1
diff --git a/code/__DEFINES/research.dm b/code/__DEFINES/research.dm
index 0bf8cb1aa00..f7660d2293b 100644
--- a/code/__DEFINES/research.dm
+++ b/code/__DEFINES/research.dm
@@ -30,17 +30,6 @@
TECHWEB_POINT_TYPE_GENERIC = "General Research",\
)
-/// The maximum amount of cash you can get from toxins experiments.
-#define TECHWEB_BOMB_CASHCAP 50000
-/// The range required to generate any cash from toxins experiments.
-#define TECHWEB_BOMB_MIN_RANGE (GLOB.MAX_EX_LIGHT_RANGE)
-/// The range required to produce the maximum amount of cash from toxins experiments.
-#define TECHWEB_BOMB_MAX_RANGE 300
-/// A constant used to scale the cash produced by toxins experiments.
-#define TECHWEB_BOMB_SCALE_CONST 300000
-/// A constant used to scale the cash produced by toxins experiments.
-#define TECHWEB_BOMB_SCALE_DIVISOR 1500
-
//! Amount of points gained per second by a single R&D server, see: [research][code/controllers/subsystem/research.dm]
#define TECHWEB_SINGLE_SERVER_INCOME 52.3
diff --git a/code/__HELPERS/atmospherics.dm b/code/__HELPERS/atmospherics.dm
index fb08e7b5495..9041e1cbec2 100644
--- a/code/__HELPERS/atmospherics.dm
+++ b/code/__HELPERS/atmospherics.dm
@@ -6,3 +6,38 @@
/proc/molar_cmp_equals(a,b,epsilon = MAXIMUM_ERROR_GAS_REMOVAL)
return (((a + epsilon) > b) && ((a - epsilon) < b))
+
+/** A simple rudimentary gasmix to information list converter. Can be used for UIs.
+ * Args:
+ * - gasmix: [/datum/gas_mixture]
+ * - name: String used to name the list, optional.
+ * Returns: A list parsed_gasmixes with the following structure:
+ * - parsed_gasmixes - Assoc List
+ * -- Key: name Value: String Desc: Gasmix Name
+ * -- Key: temperature Value: Number Desc: Temperature in kelvins
+ * -- Key: volume Value: Number Desc: Volume in liters
+ * -- Key: pressure Value: Number Desc: Pressure in kPa
+ * -- Key: ref Value: Text Desc: The reference for the instantiated gasmix.
+ * -- Key: gases Value: Assoc list Desc: List of gasses in our gasmix
+ * --- Key: gas_name Value: Gas Mole Desc: Gas Name - Gas Amount pair
+ * Returned list should always be filled with keys even if value are nulls.
+ */
+/proc/gas_mixture_parser(datum/gas_mixture/gasmix, name)
+ . = list(
+ "gases" = list(),
+ "name" = name,
+ "total_moles" = null,
+ "temperature" = null,
+ "volume"= null,
+ "pressure"= null,
+ "ref" = null,
+ )
+ if(!gasmix)
+ return
+ for(var/gas_id in gasmix.gases)
+ .["gases"][gasmix.gases[gas_id][GAS_META][META_GAS_NAME]] = gasmix.gases[gas_id][MOLES]
+ .["total_moles"] = gasmix.total_moles()
+ .["temperature"] = gasmix.temperature
+ .["volume"] = gasmix.volume
+ .["pressure"] = gasmix.return_pressure()
+ .["ref"] = REF(gasmix)
diff --git a/code/controllers/subsystem/air.dm b/code/controllers/subsystem/air.dm
index 2323a614c3b..f3da1e9f954 100644
--- a/code/controllers/subsystem/air.dm
+++ b/code/controllers/subsystem/air.dm
@@ -41,6 +41,8 @@ SUBSYSTEM_DEF(air)
var/list/turf/active_super_conductivity = list()
var/list/turf/open/high_pressure_delta = list()
var/list/atom_process = list()
+ /// Reactions which will contribute to a hotspot's size.
+ var/list/hotspot_reactions
/// A cache of objects that perisists between processing runs when resumed == TRUE. Dangerous, qdel'd objects not cleared from this may cause runtimes on processing.
var/list/currentrun = list()
@@ -82,6 +84,7 @@ SUBSYSTEM_DEF(air)
/datum/controller/subsystem/air/Initialize(timeofday)
map_loading = FALSE
gas_reactions = init_gas_reactions()
+ hotspot_reactions = init_hotspot_reactions()
setup_allturfs()
setup_atmos_machinery()
diff --git a/code/controllers/subsystem/explosions.dm b/code/controllers/subsystem/explosions.dm
index da3655ad745..81f0c54fa6e 100644
--- a/code/controllers/subsystem/explosions.dm
+++ b/code/controllers/subsystem/explosions.dm
@@ -31,7 +31,8 @@ SUBSYSTEM_DEF(explosions)
var/list/med_mov_atom = list()
var/list/high_mov_atom = list()
- var/list/explosions = list()
+ // Track how many explosions have happened.
+ var/explosion_index = 0
var/currentpart = SSAIR_PIPENETS
@@ -460,7 +461,9 @@ SUBSYSTEM_DEF(explosions)
if(GLOB.Debug2)
log_world("## DEBUG: Explosion([x0],[y0],[z0])(d[devastation_range],h[heavy_impact_range],l[light_impact_range]): Took [took] seconds.")
- SEND_GLOBAL_SIGNAL(COMSIG_GLOB_EXPLOSION, epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range)
+ explosion_index += 1
+
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_EXPLOSION, epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range, explosion_cause, explosion_index)
// Explosion SFX defines...
/// The probability that a quaking explosion will make the station creak per unit. Maths!
diff --git a/code/controllers/subsystem/research.dm b/code/controllers/subsystem/research.dm
index 0d154dd1c53..46651acfb69 100644
--- a/code/controllers/subsystem/research.dm
+++ b/code/controllers/subsystem/research.dm
@@ -55,10 +55,16 @@ SUBSYSTEM_DEF(research)
ANOMALY_CORE_FLUX = MAX_CORES_FLUX
)
+ /// Lookup list for ordnance briefers.
+ var/list/ordnance_experiments
+ /// Lookup list for scipaper partners.
+ var/list/scientific_partners
+
/datum/controller/subsystem/research/Initialize()
point_types = TECHWEB_POINT_TYPE_LIST_ASSOCIATIVE_NAMES
initialize_all_techweb_designs()
initialize_all_techweb_nodes()
+ populate_ordnance_experiments()
science_tech = new /datum/techweb/science
admin_tech = new /datum/techweb/admin
autosort_categories()
@@ -293,3 +299,17 @@ SUBSYSTEM_DEF(research)
else
techweb_boost_items[path] = list(node.id = node.boost_item_paths[path])
CHECK_TICK
+
+/datum/controller/subsystem/research/proc/populate_ordnance_experiments()
+ ordnance_experiments = list()
+ scientific_partners = list()
+
+ for (var/datum/experiment/ordnance/experiment_path as anything in subtypesof(/datum/experiment/ordnance))
+ if (initial(experiment_path.experiment_proper))
+ ordnance_experiments += new experiment_path()
+ for(var/partner_path in subtypesof(/datum/scientific_partner))
+ var/datum/scientific_partner/partner = new partner_path
+ if(!partner.accepted_experiments.len)
+ for (var/datum/experiment/ordnance/ordnance_experiment as anything in ordnance_experiments)
+ partner.accepted_experiments += ordnance_experiment.type
+ scientific_partners += partner
diff --git a/code/datums/components/atmos_reaction_recorder.dm b/code/datums/components/atmos_reaction_recorder.dm
new file mode 100644
index 00000000000..02ec672655c
--- /dev/null
+++ b/code/datums/components/atmos_reaction_recorder.dm
@@ -0,0 +1,59 @@
+
+/**
+ * Component for atmos-related atoms to be able to record and transmit their reaction_results data
+ * Use this ONLY for atoms, gasmixtures will be accessed by calling return air on the object.
+ * Add this component after gasmixes has been initialized.
+ */
+/datum/component/atmos_reaction_recorder
+ /// The list we write append each reaction tick to.
+ /// This is often a list initialized by something else (passed as a reference under Initialize).
+ var/list/copied_reaction_results
+ /// Signals we have been listening to.
+ var/list/registered_signals
+
+/**
+ * Verify that parent is indeed an atom, and then register signals.
+ * Args:
+ * - target_list (list): The list we are writing the captured reaction_results to.
+ * - reset_criteria (list): Assoc signal-source list containing signals to be registered to. We will reset if any of them are sent.
+ */
+/datum/component/atmos_reaction_recorder/Initialize(list/target_list=list(), list/reset_criteria = list())
+ . = ..()
+
+ var/atom/parent_atom = parent
+ var/datum/gas_mixture/parent_air = parent_atom?.return_air()
+ if((!istype(parent_atom) || !istype(parent_air)))
+ return COMPONENT_INCOMPATIBLE
+
+ if(islist(target_list))
+ copied_reaction_results = target_list
+ else
+ // Still can record reactions without a list, but it most likely wont be accesible outside of the component.
+ stack_trace("Atmos reaction recorder component initialized without a list.")
+ copied_reaction_results = list()
+
+ registered_signals = list()
+ RegisterSignal(parent_air, COMSIG_GASMIX_REACTED, .proc/update_data)
+ registered_signals += list(COMSIG_GASMIX_REACTED = parent_air)
+
+ for(var/signal in reset_criteria)
+ // We currently dont implement the same signal registered twice even from different sources. This allows this component to be simpler.
+ if(signal in registered_signals)
+ return COMPONENT_INCOMPATIBLE
+ RegisterSignal(reset_criteria[signal], signal, .proc/reset_data)
+ registered_signals[signal] = reset_criteria[signal]
+
+/// Fetches reaction_results and updates the list.
+/datum/component/atmos_reaction_recorder/proc/update_data(datum/gas_mixture/recorded_gasmix)
+ SIGNAL_HANDLER
+ for (var/reaction in recorded_gasmix.reaction_results)
+ copied_reaction_results[reaction] += recorded_gasmix.reaction_results[reaction]
+
+/datum/component/atmos_reaction_recorder/proc/reset_data()
+ SIGNAL_HANDLER
+ copied_reaction_results.Cut()
+
+/datum/component/atmos_reaction_recorder/UnregisterFromParent()
+ . = ..()
+ for(var/signal in registered_signals)
+ UnregisterSignal(registered_signals[signal], signal)
diff --git a/code/game/machinery/computer/arcade/arcade.dm b/code/game/machinery/computer/arcade/arcade.dm
index 97c5ce27887..69a9b80a75b 100644
--- a/code/game/machinery/computer/arcade/arcade.dm
+++ b/code/game/machinery/computer/arcade/arcade.dm
@@ -550,7 +550,7 @@ GLOBAL_LIST_INIT(arcade_prize_pool, list(
playsound(loc, 'sound/arcade/win.ogg', 50, TRUE)
if(obj_flags & EMAGGED)
- new /obj/effect/spawner/newbomb/timer(loc)
+ new /obj/effect/spawner/newbomb/plasma(loc, /obj/item/assembly/timer)
new /obj/item/clothing/head/collectable/petehat(loc)
message_admins("[ADMIN_LOOKUPFLW(usr)] has outbombed Cuban Pete and been awarded a bomb.")
log_game("[key_name(usr)] has outbombed Cuban Pete and been awarded a bomb.")
diff --git a/code/game/machinery/doppler_array.dm b/code/game/machinery/doppler_array.dm
deleted file mode 100644
index 9167c93220e..00000000000
--- a/code/game/machinery/doppler_array.dm
+++ /dev/null
@@ -1,270 +0,0 @@
-#define PRINTER_TIMEOUT 40
-
-/obj/machinery/doppler_array
- name = "tachyon-doppler array"
- desc = "A highly precise directional sensor array which measures the release of quants from decaying tachyons. The doppler shifting of the mirror-image formed by these quants can reveal the size, location and temporal affects of energetic disturbances within a large radius ahead of the array.\n"
- icon = 'icons/obj/machines/research.dmi'
- icon_state = "tdoppler"
- base_icon_state = "tdoppler"
- density = TRUE
- verb_say = "states coldly"
- var/cooldown = 10
- var/next_announce = 0
- var/max_dist = 150
- /// Number which will be part of the name of the next record, increased by one for each already created record
- var/record_number = 1
- /// Cooldown for the print function
- var/printer_ready = 0
- /// List of all explosion records in the form of /datum/data/tachyon_record
- var/list/records = list()
-
-/obj/machinery/doppler_array/Initialize(mapload)
- . = ..()
- RegisterSignal(SSdcs, COMSIG_GLOB_EXPLOSION, .proc/sense_explosion)
- RegisterSignal(src, COMSIG_MOVABLE_SET_ANCHORED, .proc/power_change)
- printer_ready = world.time + PRINTER_TIMEOUT
- // Alt clicking when unwrenched does not rotate. (likely from UI not returning the mouse click)
- // Also there is no sprite change for rotation dir, this shouldn't even have a rotate component tbh
- AddComponent(/datum/component/simple_rotation, AfterRotation = CALLBACK(src, .proc/RotationMessage))
-
-/datum/data/tachyon_record
- name = "Log Recording"
- var/timestamp
- var/coordinates = ""
- var/displacement = 0
- var/factual_radius = list()
- var/theory_radius = list()
-
-/obj/machinery/doppler_array/ui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(user, src, ui)
- if(!ui)
- ui = new(user, src, "TachyonArray", name)
- ui.open()
-
-/obj/machinery/doppler_array/ui_data(mob/user)
- var/list/data = list()
- data["records"] = list()
- for(var/datum/data/tachyon_record/R in records)
- var/list/record_data = list(
- name = R.name,
- timestamp = R.timestamp,
- coordinates = R.coordinates,
- displacement = R.displacement,
- factual_epicenter_radius = R.factual_radius["epicenter_radius"],
- factual_outer_radius = R.factual_radius["outer_radius"],
- factual_shockwave_radius = R.factual_radius["shockwave_radius"],
- theory_epicenter_radius = R.theory_radius["epicenter_radius"],
- theory_outer_radius = R.theory_radius["outer_radius"],
- theory_shockwave_radius = R.theory_radius["shockwave_radius"],
- ref = REF(R)
- )
- data["records"] += list(record_data)
- return data
-
-/obj/machinery/doppler_array/ui_act(action, list/params)
- . = ..()
- if(.)
- return
-
- switch(action)
- if("delete_record")
- var/datum/data/tachyon_record/record = locate(params["ref"]) in records
- if(!records || !(record in records))
- return
- records -= record
- return TRUE
- if("print_record")
- var/datum/data/tachyon_record/record = locate(params["ref"]) in records
- if(!records || !(record in records))
- return
- print(usr, record)
- return TRUE
-
-/obj/machinery/doppler_array/proc/print(mob/user, datum/data/tachyon_record/record)
- if(!record)
- return
- if(printer_ready < world.time)
- printer_ready = world.time + PRINTER_TIMEOUT
- new /obj/item/paper/record_printout(loc, record)
- else if(user)
- to_chat(user, span_warning("[src] is busy right now."))
-
-/obj/item/paper/record_printout
- name = "paper - Log Recording"
-
-/obj/item/paper/record_printout/Initialize(mapload, datum/data/tachyon_record/record)
- . = ..()
-
- if(record)
- name = "paper - [record.name]"
-
- info += {"
[record.name]
- - Timestamp: [record.timestamp]
- - Coordinates: [record.coordinates]
- - Displacement: [record.displacement] seconds
- - Epicenter Radius: [record.factual_radius["epicenter_radius"]]
- - Outer Radius: [record.factual_radius["outer_radius"]]
- - Shockwave Radius: [record.factual_radius["shockwave_radius"]]
"}
-
- if(length(record.theory_radius))
- info += {"- Theoretical Epicenter Radius: [record.theory_radius["epicenter_radius"]]
- - Theoretical Outer Radius: [record.theory_radius["outer_radius"]]
- - Theoretical Shockwave Radius: [record.theory_radius["shockwave_radius"]]
"}
-
- update_appearance()
-
-/obj/machinery/doppler_array/attackby(obj/item/I, mob/user, params)
- if(I.tool_behaviour == TOOL_WRENCH)
- if(!anchored && !isinspace())
- set_anchored(TRUE)
- to_chat(user, span_notice("You fasten [src]."))
- else if(anchored)
- set_anchored(FALSE)
- to_chat(user, span_notice("You unfasten [src]."))
- I.play_tool_sound(src)
- return
- return ..()
-
-/obj/machinery/doppler_array/AltClick(mob/user)
- return ..() // This hotkey is BLACKLISTED since it's used by /datum/component/simple_rotation
-
-/obj/machinery/doppler_array/proc/RotationMessage(mob/user)
- to_chat(user, span_notice("You adjust [src]'s dish to face to the [dir2text(dir)]."))
- playsound(src, 'sound/items/screwdriver2.ogg', 50, TRUE)
-
-/obj/machinery/doppler_array/proc/sense_explosion(datum/source, turf/epicenter, devastation_range, heavy_impact_range, light_impact_range,
- took, orig_dev_range, orig_heavy_range, orig_light_range)
- SIGNAL_HANDLER
-
- if(machine_stat & NOPOWER)
- return FALSE
- var/turf/zone = get_turf(src)
- if(zone.z != epicenter.z)
- return FALSE
-
- if(next_announce > world.time)
- return FALSE
- next_announce = world.time + cooldown
-
- if((get_dist(epicenter, zone) > max_dist) || !(get_dir(zone, epicenter) & dir))
- return FALSE
-
- var/datum/data/tachyon_record/R = new /datum/data/tachyon_record()
- R.name = "Log Recording #[record_number]"
- R.timestamp = station_time_timestamp()
- R.coordinates = "[epicenter.x], [epicenter.y]"
- R.displacement = took
- R.factual_radius["epicenter_radius"] = devastation_range
- R.factual_radius["outer_radius"] = heavy_impact_range
- R.factual_radius["shockwave_radius"] = light_impact_range
-
- var/list/messages = list("Explosive disturbance detected.",
- "Epicenter at: grid ([epicenter.x], [epicenter.y]). Temporal displacement of tachyons: [took] seconds.",
- "Factual: Epicenter radius: [devastation_range]. Outer radius: [heavy_impact_range]. Shockwave radius: [light_impact_range].",
- )
-
- // If the bomb was capped, say its theoretical size.
- if(devastation_range < orig_dev_range || heavy_impact_range < orig_heavy_range || light_impact_range < orig_light_range)
- messages += "Theoretical: Epicenter radius: [orig_dev_range]. Outer radius: [orig_heavy_range]. Shockwave radius: [orig_light_range]."
- R.theory_radius["epicenter_radius"] = orig_dev_range
- R.theory_radius["outer_radius"] = orig_heavy_range
- R.theory_radius["shockwave_radius"] = orig_light_range
-
- for(var/message in messages)
- say(message)
-
- record_number++
- records += R
-
- SEND_SIGNAL(src, COMSIG_DOPPLER_ARRAY_EXPLOSION_DETECTED, epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range)
-
- return TRUE
-
-/obj/machinery/doppler_array/powered()
- return anchored && ..()
-
-/obj/machinery/doppler_array/update_icon_state()
- if(machine_stat & BROKEN)
- icon_state = "[base_icon_state]-broken"
- return ..()
- icon_state = "[base_icon_state][powered() ? null : "-off"]"
- return ..()
-
-/obj/machinery/doppler_array/research
- name = "tachyon-doppler research array"
- desc = "A specialized tachyon-doppler bomb detection array that uses complex on-board software to record data for experiments."
- circuit = /obj/item/circuitboard/machine/doppler_array
- var/datum/techweb/linked_techweb
-
-/obj/machinery/doppler_array/research/Initialize(mapload)
- ..()
- linked_techweb = SSresearch.science_tech
- return INITIALIZE_HINT_LATELOAD
-
-// Late initialize to allow the server machinery to initialize first
-/obj/machinery/doppler_array/research/LateInitialize()
- . = ..()
- AddComponent(/datum/component/experiment_handler, \
- allowed_experiments = list(/datum/experiment/explosion), \
- config_mode = EXPERIMENT_CONFIG_UI, \
- config_flags = EXPERIMENT_CONFIG_ALWAYS_ACTIVE)
-
-/obj/machinery/doppler_array/research/attackby(obj/item/I, mob/user, params)
- if (default_deconstruction_screwdriver(user, "tdoppler", "tdoppler", I) || default_deconstruction_crowbar(I))
- update_icon()
- return
- return ..()
-
-/obj/machinery/doppler_array/research/sense_explosion(datum/source, turf/epicenter, devastation_range, heavy_impact_range, light_impact_range,
- took, orig_dev_range, orig_heavy_range, orig_light_range) //probably needs a way to ignore admin explosives later on
- . = ..()
- if(!.)
- return
-
- if(!istype(linked_techweb))
- say("Warning: no linked research system!")
- return
-
- var/cash_gain = 0
-
- /*****The Point Calculator*****/
- if(orig_light_range < TECHWEB_BOMB_MIN_RANGE)
- say("Explosion not large enough for profitability.")
- return
- else if(orig_light_range < TECHWEB_BOMB_MAX_RANGE)
- cash_gain = (TECHWEB_BOMB_SCALE_CONST * orig_light_range) / (TECHWEB_BOMB_SCALE_DIVISOR + orig_light_range)
- else
- cash_gain = TECHWEB_BOMB_CASHCAP
-
- /*****The Point Capper*****/
- if(cash_gain > linked_techweb.largest_bomb_value)
- if(cash_gain <= TECHWEB_BOMB_CASHCAP || linked_techweb.largest_bomb_value < TECHWEB_BOMB_CASHCAP)
- var/old_tech_largest_bomb_value = linked_techweb.largest_bomb_value //held so we can pull old before we do math
- linked_techweb.largest_bomb_value = cash_gain
- cash_gain -= old_tech_largest_bomb_value
- cash_gain = min(cash_gain, TECHWEB_BOMB_CASHCAP)
- else
- linked_techweb.largest_bomb_value = TECHWEB_BOMB_CASHCAP
- cash_gain = 1000
- var/datum/bank_account/D = SSeconomy.get_dep_account(ACCOUNT_SCI)
- if(D)
- D.adjust_money(cash_gain)
- say("Explosion details and mixture analysis sold to the highest bidder for [cash_gain] cr.")
- else //you've made smaller bombs
- say("Data already captured. Aborting.")
- return
-
-/obj/machinery/doppler_array/research/science/Initialize(mapload)
- . = ..()
- linked_techweb = SSresearch.science_tech
-
-/obj/machinery/doppler_array/research/ui_data(mob/user)
- . = ..()
- .["is_research"] = TRUE
-
-/obj/machinery/doppler_array/research/ui_act(action, list/params)
- . = ..()
- if (.)
- return
-
-#undef PRINTER_TIMEOUT
diff --git a/code/game/objects/effects/spawners/bombspawner.dm b/code/game/objects/effects/spawners/bombspawner.dm
index 2f1384cd24b..64fc60062e6 100644
--- a/code/game/objects/effects/spawners/bombspawner.dm
+++ b/code/game/objects/effects/spawners/bombspawner.dm
@@ -6,52 +6,118 @@
name = "bomb"
icon = 'icons/hud/screen_gen.dmi'
icon_state = "x"
- /// The initial temperature of the plasma tank.
- var/temp_p = 1413
- /// The initial temperature of the oxygen tank.
- var/temp_o = 141.3
- /// The initial pressure of the plasma tank.
- var/pressure_p = TANK_LEAK_PRESSURE - 1
- /// The initial pressure of the oxygen tank.
- var/pressure_o = TANK_LEAK_PRESSURE - 1
- /// The typepath of the assembly to attach to the TTV.
- var/assembly_type
+ /* Gasmixes for tank_one and tank_two of the ttv respectively.
+ * Populated on /obj/effect/spawner/newbomb/Initialize, depopulated right after by the children procs.
+ */
+ var/datum/gas_mixture/first_gasmix
+ var/datum/gas_mixture/second_gasmix
-/obj/effect/spawner/newbomb/Initialize(mapload)
+/**
+ * The part of code that actually spawns the bomb. Always call the parent's initialize first for subtypes of these.
+ *
+ * Arguments:
+ * * assembly - An assembly typepath to add to the ttv.
+ */
+/obj/effect/spawner/newbomb/Initialize(mapload, assembly = null)
. = ..()
var/obj/item/transfer_valve/ttv = new(loc)
- var/obj/item/tank/internals/plasma/plasma_tank = new(ttv)
- var/obj/item/tank/internals/oxygen/oxygen_tank = new(ttv)
-
- var/datum/gas_mixture/plasma_mix = plasma_tank.return_air()
- plasma_mix.assert_gas(/datum/gas/plasma)
- plasma_mix.gases[/datum/gas/plasma][MOLES] = pressure_p*plasma_mix.volume/(R_IDEAL_GAS_EQUATION*temp_p)
- plasma_mix.temperature = temp_p
-
- var/datum/gas_mixture/oxygen_mix = oxygen_tank.return_air()
- oxygen_mix.assert_gas(/datum/gas/oxygen)
- oxygen_mix.gases[/datum/gas/oxygen][MOLES] = pressure_o*oxygen_mix.volume/(R_IDEAL_GAS_EQUATION*temp_o)
- oxygen_mix.temperature = temp_o
-
- ttv.tank_one = plasma_tank
- ttv.tank_two = oxygen_tank
- plasma_tank.master = ttv
- oxygen_tank.master = ttv
-
- if(assembly_type)
- var/obj/item/assembly/detonator = new assembly_type(ttv)
- ttv.attached_device = detonator
- detonator.holder = ttv
-
+ ttv.tank_one = new /obj/item/tank/internals/plasma (ttv)
+ ttv.tank_two = new /obj/item/tank/internals/oxygen (ttv)
+ first_gasmix = ttv.tank_one.return_air()
+ second_gasmix = ttv.tank_two.return_air()
+ first_gasmix.remove_ratio(1)
+ second_gasmix.remove_ratio(1)
+ if(ispath(assembly, /obj/item/assembly))
+ var/obj/item/assembly/newassembly = new assembly (ttv)
+ ttv.attached_device = newassembly
+ newassembly.on_attach()
+ newassembly.holder = ttv
ttv.update_appearance()
-
return INITIALIZE_HINT_QDEL
-/obj/effect/spawner/newbomb/timer
- assembly_type = /obj/item/assembly/timer
+/obj/effect/spawner/newbomb/proc/calculate_pressure(datum/gas_mixture/gasmix, pressure)
+ return pressure * gasmix.volume/(R_IDEAL_GAS_EQUATION*gasmix.temperature)
-/obj/effect/spawner/newbomb/proximity
- assembly_type = /obj/item/assembly/prox_sensor
+/obj/effect/spawner/newbomb/plasma
-/obj/effect/spawner/newbomb/radio
- assembly_type = /obj/item/assembly/signaler
+/obj/effect/spawner/newbomb/plasma/Initialize(mapload)
+ . = ..()
+ if(!first_gasmix || !second_gasmix)
+ return
+
+ first_gasmix.temperature = 1413
+ second_gasmix.temperature = 141.3
+
+ first_gasmix.assert_gas(/datum/gas/plasma)
+ second_gasmix.assert_gas(/datum/gas/oxygen)
+
+ first_gasmix.gases[/datum/gas/plasma][MOLES] = calculate_pressure(first_gasmix, TANK_LEAK_PRESSURE - 1)
+ second_gasmix.gases[/datum/gas/oxygen][MOLES] = calculate_pressure(second_gasmix, TANK_LEAK_PRESSURE - 1)
+
+/obj/effect/spawner/newbomb/tritium
+
+/obj/effect/spawner/newbomb/tritium/Initialize(mapload, obj/item/assembly)
+ . = ..()
+ if(!first_gasmix || !second_gasmix)
+ return
+
+ first_gasmix.temperature = 8000
+ second_gasmix.temperature = 43
+
+ first_gasmix.assert_gas(/datum/gas/plasma)
+ second_gasmix.assert_gas(/datum/gas/oxygen)
+ second_gasmix.assert_gas(/datum/gas/tritium)
+
+ first_gasmix.gases[/datum/gas/plasma][MOLES] = calculate_pressure(first_gasmix, TANK_LEAK_PRESSURE - 1)
+ second_gasmix.gases[/datum/gas/oxygen][MOLES] = 0.67 * calculate_pressure(second_gasmix, TANK_LEAK_PRESSURE - 1)
+ second_gasmix.gases[/datum/gas/tritium][MOLES] = 0.33 * calculate_pressure(second_gasmix, TANK_LEAK_PRESSURE - 1)
+
+/obj/effect/spawner/newbomb/isolated_tritium
+
+/obj/effect/spawner/newbomb/isolated_tritium/Initialize(mapload)
+ . = ..()
+ if(!first_gasmix || !second_gasmix)
+ return
+
+ first_gasmix.temperature = FIRE_MINIMUM_TEMPERATURE_TO_EXIST + 1
+ second_gasmix.temperature = FIRE_MINIMUM_TEMPERATURE_TO_EXIST + 1
+
+ first_gasmix.assert_gas(/datum/gas/hypernoblium)
+ first_gasmix.assert_gas(/datum/gas/tritium)
+ second_gasmix.assert_gas(/datum/gas/oxygen)
+
+ first_gasmix.gases[/datum/gas/hypernoblium][MOLES] = REACTION_OPPRESSION_THRESHOLD - 0.01
+ first_gasmix.gases[/datum/gas/tritium][MOLES] = 0.5 * calculate_pressure(first_gasmix, TANK_LEAK_PRESSURE - 1)
+ second_gasmix.gases[/datum/gas/oxygen][MOLES] = calculate_pressure(second_gasmix, TANK_LEAK_PRESSURE-1)
+
+/obj/effect/spawner/newbomb/noblium
+
+/obj/effect/spawner/newbomb/noblium/Initialize(mapload)
+ . = ..()
+ if(!first_gasmix || !second_gasmix)
+ return
+
+ first_gasmix.temperature = 2.7
+ second_gasmix.temperature = 2.7
+
+ first_gasmix.assert_gas(/datum/gas/nitrogen)
+ second_gasmix.assert_gas(/datum/gas/tritium)
+
+ first_gasmix.gases[/datum/gas/nitrogen][MOLES] = calculate_pressure(first_gasmix, TANK_LEAK_PRESSURE - 1)
+ second_gasmix.gases[/datum/gas/tritium][MOLES] = calculate_pressure(second_gasmix, TANK_LEAK_PRESSURE - 1)
+
+/obj/effect/spawner/newbomb/pressure
+
+/obj/effect/spawner/newbomb/pressure/Initialize(mapload)
+ . = ..()
+ if(!first_gasmix || !second_gasmix)
+ return
+
+ first_gasmix.temperature = 20000
+ second_gasmix.temperature = 2.7
+
+ first_gasmix.assert_gas(/datum/gas/hypernoblium)
+ second_gasmix.assert_gas(/datum/gas/tritium)
+
+ first_gasmix.gases[/datum/gas/hypernoblium][MOLES] = calculate_pressure(first_gasmix, TANK_LEAK_PRESSURE - 1)
+ second_gasmix.gases[/datum/gas/tritium][MOLES] = calculate_pressure(second_gasmix, TANK_LEAK_PRESSURE - 1)
diff --git a/code/game/objects/items/circuitboards/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machine_circuitboards.dm
index 2c40dadcb23..15011c2bef4 100644
--- a/code/game/objects/items/circuitboards/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machine_circuitboards.dm
@@ -1294,7 +1294,7 @@
/obj/item/circuitboard/machine/doppler_array
name = "Tachyon-Doppler Research Array (Machine Board)"
greyscale_colors = CIRCUIT_COLOR_SCIENCE
- build_path = /obj/machinery/doppler_array/research
+ build_path = /obj/machinery/doppler_array
req_components = list(
/obj/item/stock_parts/micro_laser = 2,
/obj/item/stock_parts/scanning_module = 4)
@@ -1321,3 +1321,22 @@
build_path = /obj/machinery/ecto_sniffer
req_components = list(
/obj/item/stock_parts/scanning_module = 1)
+
+/obj/item/circuitboard/machine/anomaly_refinery
+ name = "Anomaly Refinery (Machine Board)"
+ greyscale_colors = CIRCUIT_COLOR_SCIENCE
+ build_path = /obj/machinery/research/anomaly_refinery
+ req_components = list(
+ /obj/item/stack/sheet/plasteel = 15,
+ /obj/item/stock_parts/scanning_module = 1,
+ /obj/item/stock_parts/manipulator = 1,
+ )
+
+/obj/item/circuitboard/machine/tank_compressor
+ name = "Tank Compressor (Machine Board)"
+ greyscale_colors = CIRCUIT_COLOR_SCIENCE
+ build_path = /obj/machinery/atmospherics/components/binary/tank_compressor
+ req_components = list(
+ /obj/item/stack/sheet/plasteel = 5,
+ /obj/item/stock_parts/scanning_module = 4,
+ )
diff --git a/code/game/objects/items/devices/transfer_valve.dm b/code/game/objects/items/devices/transfer_valve.dm
index c2363794c90..8009ffa92cd 100644
--- a/code/game/objects/items/devices/transfer_valve.dm
+++ b/code/game/objects/items/devices/transfer_valve.dm
@@ -133,24 +133,24 @@
. += "proxy_beam"
-/obj/item/transfer_valve/proc/merge_gases(datum/gas_mixture/target, change_volume = TRUE)
- var/target_self = FALSE
- var/datum/gas_mixture/mix_one = tank_one.return_air()
- var/datum/gas_mixture/mix_two = tank_two.return_air()
- if(!target || (target == mix_one))
- target = mix_two
- if(target == mix_two)
- target_self = TRUE
+/// Merge both gases into a single tank. Combine the volume by default. If target tank isn't specified default to tank_two
+/obj/item/transfer_valve/proc/merge_gases(obj/item/tank/target, change_volume = TRUE)
+ if(!target)
+ target = tank_two
+
+ if(!istype(target) || (target != tank_one && target != tank_two))
+ return FALSE
+
+ // Throw both tanks into processing queue
+ var/datum/gas_mixture/target_mix = target.return_air()
+ var/datum/gas_mixture/other_mix
+ other_mix = (target == tank_one ? tank_two : tank_one).return_air()
+
if(change_volume)
- if(!target_self)
- target.volume += tank_two.volume
- target.volume += mix_one.volume
- var/datum/gas_mixture/temp
- temp = mix_one.remove_ratio(1)
- target.merge(temp)
- if(!target_self)
- temp = mix_two.remove_ratio(1)
- target.merge(temp)
+ target_mix.volume += other_mix.volume
+
+ target_mix.merge(other_mix.remove_ratio(1))
+ return TRUE
/obj/item/transfer_valve/proc/split_gases()
if (!valve_open || !tank_one || !tank_two)
@@ -168,9 +168,8 @@
Exadv1: I know this isn't how it's going to work, but this was just to check
it explodes properly when it gets a signal (and it does).
*/
-/obj/item/transfer_valve/proc/toggle_valve()
+/obj/item/transfer_valve/proc/toggle_valve(obj/item/tank/target, change_volume = TRUE)
if(!valve_open && tank_one && tank_two)
- valve_open = TRUE
var/turf/bombturf = get_turf(src)
var/attachment
@@ -201,7 +200,11 @@
message_admins(admin_bomb_message)
log_game("Bomb valve opened in [AREACOORD(bombturf)][attachment_message][bomber_message]")
- merge_gases()
+ valve_open = merge_gases(target, change_volume)
+
+ if(!valve_open)
+ stack_trace("TTV gas merging failed.")
+
for(var/i in 1 to 6)
addtimer(CALLBACK(src, /atom/.proc/update_appearance), 20 + (i - 1) * 10)
diff --git a/code/game/objects/items/tanks/tanks.dm b/code/game/objects/items/tanks/tanks.dm
index d8b7fd91763..093048267da 100644
--- a/code/game/objects/items/tanks/tanks.dm
+++ b/code/game/objects/items/tanks/tanks.dm
@@ -38,6 +38,10 @@
var/tank_holder_icon_state = "holder_generic"
///Used by process() to track if there's a reason to process each tick
var/excited = TRUE
+ /// How our particular tank explodes.
+ var/list/explosion_info
+ /// List containing reactions happening inside our tank.
+ var/list/reaction_info
/obj/item/tank/ui_action_click(mob/user)
toggle_internals(user)
@@ -80,21 +84,31 @@
populate_gas()
+ reaction_info = list()
+ explosion_info = list()
+
+ AddComponent(/datum/component/atmos_reaction_recorder, reset_criteria = list(COMSIG_GASMIX_MERGING = air_contents, COMSIG_GASMIX_REMOVING = air_contents), target_list = reaction_info)
+
+ // This is separate from the reaction recorder.
+ // In this case we are only listening to determine if the tank is overpressurized but not destroyed.
+ RegisterSignal(air_contents, COMSIG_GASMIX_MERGED, .proc/merging_information)
+
START_PROCESSING(SSobj, src)
/obj/item/tank/proc/populate_gas()
return
-/obj/item/tank/Destroy()
- air_contents = null
- STOP_PROCESSING(SSobj, src)
- return ..()
-
/obj/item/tank/ComponentInitialize()
. = ..()
if(tank_holder_icon_state)
AddComponent(/datum/component/container_item/tank_holder, tank_holder_icon_state)
+/obj/item/tank/Destroy()
+ UnregisterSignal(air_contents, COMSIG_GASMIX_MERGED)
+ air_contents = null
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
/obj/item/tank/examine(mob/user)
var/obj/icon = src
. = ..()
@@ -324,4 +338,12 @@
dyn_explosion(src, power, flash_range = 1.5, ignorecap = FALSE)
return ..()
+/obj/item/tank/proc/merging_information()
+ SIGNAL_HANDLER
+ if(air_contents.return_pressure() > TANK_FRAGMENT_PRESSURE)
+ explosion_info += TANK_MERGE_OVERPRESSURE
+
+/obj/item/tank/proc/explosion_information()
+ return list(TANK_RESULTS_REACTION = reaction_info, TANK_RESULTS_MISC = explosion_info)
+
#undef ASSUME_AIR_DT_FACTOR
diff --git a/code/modules/atmospherics/environmental/LINDA_fire.dm b/code/modules/atmospherics/environmental/LINDA_fire.dm
index 6d0ea3fa379..659816a51be 100644
--- a/code/modules/atmospherics/environmental/LINDA_fire.dm
+++ b/code/modules/atmospherics/environmental/LINDA_fire.dm
@@ -1,4 +1,11 @@
+/// Returns reactions which will contribute to a hotspot's size.
+/proc/init_hotspot_reactions()
+ var/list/fire_reactions = list()
+ for (var/datum/gas_reaction/reaction as anything in subtypesof(/datum/gas_reaction))
+ if(initial(reaction.expands_hotspot))
+ fire_reactions += reaction
+ return fire_reactions
/atom/proc/temperature_expose(datum/gas_mixture/air, exposed_temperature, exposed_volume)
return null
@@ -109,6 +116,7 @@
*/
/obj/effect/hotspot/proc/perform_exposure()
var/turf/open/location = loc
+ var/datum/gas_mixture/reference
if(!istype(location) || !(location.air))
return
@@ -118,18 +126,23 @@
//Passive mode
if(bypassing)
- volume = location.air.reaction_results["fire"]*FIRE_GROWTH_RATE
- temperature = location.air.temperature
+ reference = location.air // Our color and volume will depend on the turf's gasmix
//Active mode
else
var/datum/gas_mixture/affected = location.air.remove_ratio(volume/location.air.volume)
if(affected) //in case volume is 0
+ reference = affected // Our color and volume will depend on this small sparked gasmix
affected.temperature = temperature
affected.react(src)
- temperature = affected.temperature
- volume = affected.reaction_results["fire"]*FIRE_GROWTH_RATE
location.assume_air(affected)
+ if(reference)
+ volume = 0
+ var/list/cached_results = reference.reaction_results
+ for (var/reaction in SSair.hotspot_reactions)
+ volume += cached_results[reaction] * FIRE_GROWTH_RATE
+ temperature = reference.temperature
+
// Handles the burning of atoms.
for(var/A in location)
var/atom/AT = A
diff --git a/code/modules/atmospherics/gasmixtures/gas_mixture.dm b/code/modules/atmospherics/gasmixtures/gas_mixture.dm
index 7dff950645b..335c1c0da08 100644
--- a/code/modules/atmospherics/gasmixtures/gas_mixture.dm
+++ b/code/modules/atmospherics/gasmixtures/gas_mixture.dm
@@ -25,7 +25,7 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache())
var/tmp/temperature_archived = 0
var/volume = CELL_VOLUME //liters
var/last_share = 0
- /// The fire key contains information that might determine the volume of hotspots.
+ /// Tells us what reactions have happened in our gasmix. Assoc list of reaction - moles reacted pair.
var/list/reaction_results
/// Used for analyzer feedback - not initialized until its used
var/list/analyzer_results
@@ -164,6 +164,7 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache())
ASSERT_GAS(giver_id, src)
cached_gases[giver_id][MOLES] += giver_gases[giver_id][MOLES]
+ SEND_SIGNAL(src, COMSIG_GASMIX_MERGED)
return TRUE
///Proportionally removes amount of gas from the gas_mixture.
@@ -186,6 +187,7 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache())
cached_gases[id][MOLES] -= removed_gases[id][MOLES]
garbage_collect()
+ SEND_SIGNAL(src, COMSIG_GASMIX_REMOVED)
return removed
///Proportionally removes amount of gas from the gas_mixture.
@@ -208,6 +210,7 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache())
garbage_collect()
+ SEND_SIGNAL(src, COMSIG_GASMIX_REMOVED)
return removed
///Removes an amount of a specific gas from the gas_mixture.
@@ -534,6 +537,7 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache())
if(.) //If we changed the mix to any degree
garbage_collect()
+ SEND_SIGNAL(src, COMSIG_GASMIX_REACTED)
/**
diff --git a/code/modules/atmospherics/gasmixtures/reactions.dm b/code/modules/atmospherics/gasmixtures/reactions.dm
index 83aad86e6fe..548c1aab062 100644
--- a/code/modules/atmospherics/gasmixtures/reactions.dm
+++ b/code/modules/atmospherics/gasmixtures/reactions.dm
@@ -1,4 +1,5 @@
-//All defines used in reactions are located in ..\__DEFINES\reactions.dm
+//Most other defines used in reactions are located in ..\__DEFINES\reactions.dm
+#define SET_REACTION_RESULTS(amount) air.reaction_results[type] = amount
/proc/init_gas_reactions()
var/list/priority_reactions = list()
@@ -52,6 +53,8 @@
var/priority_group
var/name = "reaction"
var/id = "r"
+ /// Whether the presence of our reaction should make fires bigger or not.
+ var/expands_hotspot = FALSE
/datum/gas_reaction/New()
init_reqs()
@@ -75,10 +78,12 @@
. = NO_REACTION
if (air.temperature <= WATER_VAPOR_FREEZE)
if(location?.freeze_turf())
+ SET_REACTION_RESULTS(0)
. = REACTING
else if(air.temperature <= T20C + 10)
if(location?.water_vapor_gas_act())
air.gases[/datum/gas/water_vapor][MOLES] -= MOLES_GAS_VISIBLE
+ SET_REACTION_RESULTS(MOLES_GAS_VISIBLE)
. = REACTING
//N2O decomposition.
@@ -100,31 +105,32 @@
var/temperature = air.temperature
var/burned_fuel = 0
-
burned_fuel = max(0,0.00002 * (temperature - (0.00001 * (temperature**2)))) * cached_gases[/datum/gas/nitrous_oxide][MOLES]
- if(cached_gases[/datum/gas/nitrous_oxide][MOLES] - burned_fuel < 0)
+ if(!burned_fuel || cached_gases[/datum/gas/nitrous_oxide][MOLES] - burned_fuel < 0)
return NO_REACTION
+
cached_gases[/datum/gas/nitrous_oxide][MOLES] -= burned_fuel
- if(burned_fuel)
- energy_released += (N2O_DECOMPOSITION_ENERGY_RELEASED * burned_fuel)
+ ASSERT_GAS(/datum/gas/oxygen, air)
+ cached_gases[/datum/gas/oxygen][MOLES] += burned_fuel * 0.5
+ ASSERT_GAS(/datum/gas/nitrogen, air)
+ cached_gases[/datum/gas/nitrogen][MOLES] += burned_fuel
- ASSERT_GAS(/datum/gas/oxygen, air)
- cached_gases[/datum/gas/oxygen][MOLES] += burned_fuel * 0.5
- ASSERT_GAS(/datum/gas/nitrogen, air)
- cached_gases[/datum/gas/nitrogen][MOLES] += burned_fuel
+ energy_released += (N2O_DECOMPOSITION_ENERGY_RELEASED * burned_fuel)
+ SET_REACTION_RESULTS(burned_fuel)
- var/new_heat_capacity = air.heat_capacity()
- if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
- air.temperature = (temperature * old_heat_capacity + energy_released) / new_heat_capacity
- return REACTING
- return NO_REACTION
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.temperature = (temperature * old_heat_capacity + energy_released) / new_heat_capacity
+
+ return REACTING
//tritium combustion: combustion of oxygen and tritium (treated as hydrocarbons). creates hotspots. exothermic
/datum/gas_reaction/tritfire
priority_group = PRIORITY_FIRE
name = "Tritium Combustion"
id = "tritfire"
+ expands_hotspot = TRUE
/datum/gas_reaction/tritfire/init_reqs()
requirements = list(
@@ -138,8 +144,6 @@
var/old_heat_capacity = air.heat_capacity()
var/list/cached_gases = air.gases //this speeds things up because accessing datum vars is slow
var/temperature = air.temperature
- var/list/cached_results = air.reaction_results
- cached_results["fire"] = 0
var/turf/open/location
if(istype(holder,/datum/pipeline)) //Find the tile the reaction is occuring on, or a random part of the network if it's a pipenet.
var/datum/pipeline/pipenet = holder
@@ -155,7 +159,7 @@
cached_gases[/datum/gas/water_vapor][MOLES] += burned_fuel / TRITIUM_BURN_OXY_FACTOR
energy_released += (FIRE_HYDROGEN_ENERGY_WEAK * burned_fuel)
- cached_results["fire"] += burned_fuel
+ SET_REACTION_RESULTS(burned_fuel)
else
burned_fuel = cached_gases[/datum/gas/tritium][MOLES]
@@ -167,7 +171,7 @@
cached_gases[/datum/gas/water_vapor][MOLES] += burned_fuel / TRITIUM_BURN_TRIT_FACTOR
energy_released += (FIRE_HYDROGEN_ENERGY_RELEASED * burned_fuel)
- cached_results["fire"] += burned_fuel * 10
+ SET_REACTION_RESULTS(burned_fuel * 10) // This is actually a lie. We use 10x less moles here but make 10x more energy.
if(location && prob(10) && burned_fuel > TRITIUM_MINIMUM_RADIATION_ENERGY && energy_released > FIRE_HYDROGEN_ENERGY_RELEASED * air.volume / 2500) //Reduces chances of radiation getting released from the tritium getting formed from oxygen rich plasmafires in waste.
radiation_pulse(location, max_range = min(6 + sqrt(energy_released / FIRE_HYDROGEN_ENERGY_RELEASED) / 4, 20), threshold = 15 * INVERSE(15 + energy_released / FIRE_HYDROGEN_ENERGY_RELEASED), chance = 100 * (1 - 0.5 ** (energy_released / (600 * FIRE_HYDROGEN_ENERGY_RELEASED))))
@@ -182,13 +186,14 @@
if(temperature > FIRE_MINIMUM_TEMPERATURE_TO_EXIST)
location.hotspot_expose(temperature, CELL_VOLUME)
- return cached_results["fire"] ? REACTING : NO_REACTION
+ return energy_released ? REACTING : NO_REACTION
//plasma combustion: combustion of oxygen and plasma (treated as hydrocarbons). creates hotspots. exothermic
/datum/gas_reaction/plasmafire
priority_group = PRIORITY_FIRE
name = "Plasma Combustion"
id = "plasmafire"
+ expands_hotspot = TRUE
/datum/gas_reaction/plasmafire/init_reqs()
requirements = list(
@@ -202,8 +207,6 @@
var/old_heat_capacity = air.heat_capacity()
var/list/cached_gases = air.gases //this speeds things up because accessing datum vars is slow
var/temperature = air.temperature
- var/list/cached_results = air.reaction_results
- cached_results["fire"] = 0
var/turf/open/location = isturf(holder) ? holder : null
//Handle plasma burning
@@ -242,7 +245,7 @@
energy_released += FIRE_PLASMA_ENERGY_RELEASED * (plasma_burn_rate)
- cached_results["fire"] += (plasma_burn_rate) * (1 + oxygen_burn_rate)
+ SET_REACTION_RESULTS((plasma_burn_rate) * (1 + oxygen_burn_rate))
if(energy_released > 0)
var/new_heat_capacity = air.heat_capacity()
@@ -255,7 +258,7 @@
if(temperature > FIRE_MINIMUM_TEMPERATURE_TO_EXIST)
location.hotspot_expose(temperature, CELL_VOLUME)
- return cached_results["fire"] ? REACTING : NO_REACTION
+ return energy_released ? REACTING : NO_REACTION
//freon reaction (is not a fire yet)
/datum/gas_reaction/freonfire
@@ -308,6 +311,7 @@
new /obj/item/stack/sheet/hot_ice(location)
energy_released += FIRE_FREON_ENERGY_RELEASED * (freon_burn_rate)
+ SET_REACTION_RESULTS(freon_burn_rate)
. = REACTING
if(energy_released < 0)
@@ -320,6 +324,7 @@
priority_group = PRIORITY_FIRE
name = "Hydrogen Combustion"
id = "h2fire"
+ expands_hotspot = TRUE
/datum/gas_reaction/h2fire/init_reqs()
requirements = list(
@@ -333,8 +338,6 @@
var/old_heat_capacity = air.heat_capacity()
var/list/cached_gases = air.gases //this speeds things up because accessing datum vars is slow
var/temperature = air.temperature
- var/list/cached_results = air.reaction_results
- cached_results["fire"] = 0
var/turf/open/location = isturf(holder) ? holder : null
var/burned_fuel = 0
if(cached_gases[/datum/gas/oxygen][MOLES] < cached_gases[/datum/gas/hydrogen][MOLES] || MINIMUM_H2_OXYBURN_ENERGY > air.thermal_energy())
@@ -345,7 +348,7 @@
cached_gases[/datum/gas/water_vapor][MOLES] += burned_fuel / HYDROGEN_BURN_OXY_FACTOR
energy_released += (FIRE_HYDROGEN_ENERGY_WEAK * burned_fuel)
- cached_results["fire"] += burned_fuel
+ SET_REACTION_RESULTS(burned_fuel)
else
burned_fuel = cached_gases[/datum/gas/hydrogen][MOLES]
@@ -357,7 +360,7 @@
cached_gases[/datum/gas/water_vapor][MOLES] += burned_fuel / HYDROGEN_BURN_H2_FACTOR
energy_released += (FIRE_HYDROGEN_ENERGY_RELEASED * burned_fuel)
- cached_results["fire"] += burned_fuel * 10
+ SET_REACTION_RESULTS(burned_fuel * 10) // This is actually a lie. We use 10x less moles here but make 10x more energy.
if(energy_released > 0)
var/new_heat_capacity = air.heat_capacity()
@@ -370,7 +373,7 @@
if(temperature > FIRE_MINIMUM_TEMPERATURE_TO_EXIST)
location.hotspot_expose(temperature, CELL_VOLUME)
- return cached_results["fire"] ? REACTING : NO_REACTION
+ return energy_released ? REACTING : NO_REACTION
/datum/gas_reaction/nitrousformation //formationn of n2o, exothermic, requires bz as catalyst
priority_group = PRIORITY_FORMATION
@@ -401,6 +404,8 @@
cached_gases[/datum/gas/nitrogen][MOLES] -= heat_efficency * 2
cached_gases[/datum/gas/nitrous_oxide][MOLES] += heat_efficency
+ SET_REACTION_RESULTS(heat_efficency)
+
if(energy_used > 0)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -436,6 +441,8 @@
cached_gases[/datum/gas/hydrogen][MOLES] += heat_efficency
cached_gases[/datum/gas/nitrogen][MOLES] += heat_efficency
+ SET_REACTION_RESULTS(heat_efficency)
+
if(energy_produced> 0)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -462,15 +469,19 @@
var/old_heat_capacity = air.heat_capacity()
var/heat_efficency = min(temperature / (FIRE_MINIMUM_TEMPERATURE_TO_EXIST * 8), cached_gases[/datum/gas/tritium][MOLES], cached_gases[/datum/gas/nitrogen][MOLES], cached_gases[/datum/gas/bz][MOLES] * INVERSE(0.05))
var/energy_used = heat_efficency * NITRIUM_FORMATION_ENERGY
- ASSERT_GAS(/datum/gas/nitrium, air)
+
if ((cached_gases[/datum/gas/tritium][MOLES] - heat_efficency < 0 ) || (cached_gases[/datum/gas/nitrogen][MOLES] - heat_efficency < 0) || (cached_gases[/datum/gas/bz][MOLES] - heat_efficency * 0.05 < 0)) //Shouldn't produce gas from nothing.
return NO_REACTION
+ ASSERT_GAS(/datum/gas/nitrium, air)
+
cached_gases[/datum/gas/tritium][MOLES] -= heat_efficency
cached_gases[/datum/gas/nitrogen][MOLES] -= heat_efficency
cached_gases[/datum/gas/bz][MOLES] -= heat_efficency * 0.05 //bz gets consumed to balance the nitrium production and not make it too common and/or easy
cached_gases[/datum/gas/nitrium][MOLES] += heat_efficency
+ SET_REACTION_RESULTS(heat_efficency)
+
if(energy_used > 0)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -497,6 +508,7 @@
// This slows down in relation to pressure, very quickly. Please don't expect it to be anything more then a snail
var/reaction_efficency = min(1 / ((pressure / (0.1 * ONE_ATMOSPHERE)) * (max(cached_gases[/datum/gas/plasma][MOLES] / cached_gases[/datum/gas/nitrous_oxide][MOLES], 1))), cached_gases[/datum/gas/nitrous_oxide][MOLES], cached_gases[/datum/gas/plasma][MOLES] * INVERSE(2))
var/energy_released = 2 * reaction_efficency * FIRE_CARBON_ENERGY_RELEASED
+
if ((cached_gases[/datum/gas/nitrous_oxide][MOLES] - reaction_efficency < 0 )|| (cached_gases[/datum/gas/plasma][MOLES] - (2 * reaction_efficency) < 0) || energy_released <= 0) //Shouldn't produce gas from nothing.
return NO_REACTION
@@ -509,6 +521,8 @@
cached_gases[/datum/gas/nitrous_oxide][MOLES] -= reaction_efficency
cached_gases[/datum/gas/plasma][MOLES] -= 2 * reaction_efficency
+ SET_REACTION_RESULTS(reaction_efficency)
+
if(energy_released > 0)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -534,6 +548,7 @@
var/old_heat_capacity = air.heat_capacity()
var/heat_efficency = min(temperature / (FIRE_MINIMUM_TEMPERATURE_TO_EXIST * 10), cached_gases[/datum/gas/plasma][MOLES] * INVERSE(1.5), cached_gases[/datum/gas/carbon_dioxide][MOLES] * INVERSE(0.75), cached_gases[/datum/gas/bz][MOLES] * INVERSE(0.25))
var/energy_used = heat_efficency * 100
+
if ((cached_gases[/datum/gas/plasma][MOLES] - heat_efficency * 1.5 < 0 ) || (cached_gases[/datum/gas/carbon_dioxide][MOLES] - heat_efficency * 0.75 < 0) || (cached_gases[/datum/gas/bz][MOLES] - heat_efficency * 0.25 < 0)) //Shouldn't produce gas from nothing.
return NO_REACTION
@@ -543,6 +558,8 @@
cached_gases[/datum/gas/bz][MOLES] -= heat_efficency * 0.25
cached_gases[/datum/gas/freon][MOLES] += heat_efficency * 2.5
+ SET_REACTION_RESULTS(heat_efficency * 2.5)
+
if(energy_used > 0)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -568,6 +585,7 @@
var/old_heat_capacity = air.heat_capacity()
var/nob_formed = min((cached_gases[/datum/gas/nitrogen][MOLES] + cached_gases[/datum/gas/tritium][MOLES]) * 0.01, cached_gases[/datum/gas/tritium][MOLES] * INVERSE(5), cached_gases[/datum/gas/nitrogen][MOLES] * INVERSE(10))
var/energy_produced = nob_formed * (NOBLIUM_FORMATION_ENERGY / (max(cached_gases[/datum/gas/bz][MOLES], 1)))
+
if ((cached_gases[/datum/gas/tritium][MOLES] - 5 * nob_formed < 0) || (cached_gases[/datum/gas/nitrogen][MOLES] - 10 * nob_formed < 0))
return NO_REACTION
@@ -575,6 +593,8 @@
cached_gases[/datum/gas/nitrogen][MOLES] -= 10 * nob_formed
cached_gases[/datum/gas/hypernoblium][MOLES] += nob_formed
+ SET_REACTION_RESULTS(nob_formed)
+
if (nob_formed)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -595,6 +615,7 @@
/datum/gas_reaction/miaster/react(datum/gas_mixture/air, datum/holder)
var/list/cached_gases = air.gases
+
// As the name says it, it needs to be dry
if(cached_gases[/datum/gas/water_vapor] && cached_gases[/datum/gas/water_vapor][MOLES] / air.total_moles() > 0.1)
return NO_REACTION
@@ -605,6 +626,8 @@
ASSERT_GAS(/datum/gas/oxygen, air)
cached_gases[/datum/gas/oxygen][MOLES] += cleaned_air
+ SET_REACTION_RESULTS(cleaned_air)
+
//Possibly burning a bit of organic matter through maillard reaction, so a *tiny* bit more heat would be understandable
air.temperature += cleaned_air * 0.002
@@ -629,13 +652,17 @@
var/old_heat_capacity = air.heat_capacity()
var/heat_efficency = min(temperature * 0.01, cached_gases[/datum/gas/tritium][MOLES] * INVERSE(4), cached_gases[/datum/gas/bz][MOLES] * INVERSE(0.25))
var/energy_used = heat_efficency * 300
+
ASSERT_GAS(/datum/gas/halon, air)
if ((cached_gases[/datum/gas/tritium][MOLES] - heat_efficency * 4 < 0 ) || (cached_gases[/datum/gas/bz][MOLES] - heat_efficency * 0.25 < 0)) //Shouldn't produce gas from nothing.
return NO_REACTION
+
cached_gases[/datum/gas/tritium][MOLES] -= heat_efficency * 4
cached_gases[/datum/gas/bz][MOLES] -= heat_efficency * 0.25
cached_gases[/datum/gas/halon][MOLES] += heat_efficency * 4.25
+ SET_REACTION_RESULTS(heat_efficency * 4.25)
+
if(energy_used)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -661,13 +688,17 @@
var/old_heat_capacity = air.heat_capacity()
var/heat_efficency = min(temperature * 0.3, cached_gases[/datum/gas/freon][MOLES] * INVERSE(2.75), cached_gases[/datum/gas/bz][MOLES] * INVERSE(0.25))
var/energy_used = heat_efficency * 9000
+
ASSERT_GAS(/datum/gas/healium, air)
if ((cached_gases[/datum/gas/freon][MOLES] - heat_efficency * 2.75 < 0 ) || (cached_gases[/datum/gas/bz][MOLES] - heat_efficency * 0.25 < 0)) //Shouldn't produce gas from nothing.
return NO_REACTION
+
cached_gases[/datum/gas/freon][MOLES] -= heat_efficency * 2.75
cached_gases[/datum/gas/bz][MOLES] -= heat_efficency * 0.25
cached_gases[/datum/gas/healium][MOLES] += heat_efficency * 3
+ SET_REACTION_RESULTS(heat_efficency * 3)
+
if(energy_used)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -693,13 +724,17 @@
var/old_heat_capacity = air.heat_capacity()
var/heat_efficency = min(temperature * 0.005, cached_gases[/datum/gas/pluoxium][MOLES] * INVERSE(0.2), cached_gases[/datum/gas/hydrogen][MOLES] * INVERSE(2))
var/energy_used = heat_efficency * 650
+
ASSERT_GAS(/datum/gas/proto_nitrate, air)
if ((cached_gases[/datum/gas/pluoxium][MOLES] - heat_efficency * 0.2 < 0 ) || (cached_gases[/datum/gas/hydrogen][MOLES] - heat_efficency * 2 < 0)) //Shouldn't produce gas from nothing.
return NO_REACTION
+
cached_gases[/datum/gas/hydrogen][MOLES] -= heat_efficency * 2
cached_gases[/datum/gas/pluoxium][MOLES] -= heat_efficency * 0.2
cached_gases[/datum/gas/proto_nitrate][MOLES] += heat_efficency * 2.2
+ SET_REACTION_RESULTS(heat_efficency * 2.2)
+
if(energy_used > 0)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -725,13 +760,17 @@
var/old_heat_capacity = air.heat_capacity()
var/heat_efficency = min(temperature * 0.000005, cached_gases[/datum/gas/hypernoblium][MOLES] * INVERSE(0.01), cached_gases[/datum/gas/nitrium][MOLES] * INVERSE(0.5))
var/energy_used = heat_efficency * 5000
- ASSERT_GAS(/datum/gas/zauker, air)
+
if ((cached_gases[/datum/gas/hypernoblium][MOLES] - heat_efficency * 0.01 < 0 ) || (cached_gases[/datum/gas/nitrium][MOLES] - heat_efficency * 0.5 < 0)) //Shouldn't produce gas from nothing.
return NO_REACTION
+
+ ASSERT_GAS(/datum/gas/zauker, air)
cached_gases[/datum/gas/hypernoblium][MOLES] -= heat_efficency * 0.01
cached_gases[/datum/gas/nitrium][MOLES] -= heat_efficency * 0.5
cached_gases[/datum/gas/zauker][MOLES] += heat_efficency * 0.5
+ SET_REACTION_RESULTS(heat_efficency * 0.5)
+
if(energy_used)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -756,13 +795,17 @@
var/old_heat_capacity = air.heat_capacity()
var/heat_efficency = min(temperature / ( FIRE_MINIMUM_TEMPERATURE_TO_EXIST * 10), cached_gases[/datum/gas/halon][MOLES], cached_gases[/datum/gas/oxygen][MOLES] * INVERSE(20))
var/energy_used = heat_efficency * 2500
- ASSERT_GAS(/datum/gas/carbon_dioxide, air)
+
if ((cached_gases[/datum/gas/halon][MOLES] - heat_efficency < 0 ) || (cached_gases[/datum/gas/oxygen][MOLES] - heat_efficency * 20 < 0)) //Shouldn't produce gas from nothing.
return NO_REACTION
+
+ ASSERT_GAS(/datum/gas/carbon_dioxide, air)
cached_gases[/datum/gas/halon][MOLES] -= heat_efficency
cached_gases[/datum/gas/oxygen][MOLES] -= heat_efficency * 20
cached_gases[/datum/gas/carbon_dioxide][MOLES] += heat_efficency * 5
+ SET_REACTION_RESULTS(heat_efficency * 5)
+
if(energy_used)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -786,24 +829,26 @@
var/list/cached_gases = air.gases //this speeds things up because accessing datum vars is slow
var/temperature = air.temperature
var/burned_fuel = 0
+
burned_fuel = min(20, cached_gases[/datum/gas/nitrogen][MOLES], cached_gases[/datum/gas/zauker][MOLES])
- if(cached_gases[/datum/gas/zauker][MOLES] - burned_fuel < 0)
+ if(!burned_fuel || cached_gases[/datum/gas/zauker][MOLES] - burned_fuel < 0)
return NO_REACTION
+
+ energy_released += (460 * burned_fuel)
+
cached_gases[/datum/gas/zauker][MOLES] -= burned_fuel
- if(burned_fuel)
- energy_released += (460 * burned_fuel)
+ ASSERT_GAS(/datum/gas/oxygen, air)
+ ASSERT_GAS(/datum/gas/nitrogen, air)
+ cached_gases[/datum/gas/oxygen][MOLES] += burned_fuel * 0.3
+ cached_gases[/datum/gas/nitrogen][MOLES] += burned_fuel * 0.7
- ASSERT_GAS(/datum/gas/oxygen, air)
- ASSERT_GAS(/datum/gas/nitrogen, air)
- cached_gases[/datum/gas/oxygen][MOLES] += burned_fuel * 0.3
- cached_gases[/datum/gas/nitrogen][MOLES] += burned_fuel * 0.7
+ SET_REACTION_RESULTS(burned_fuel)
- var/new_heat_capacity = air.heat_capacity()
- if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
- air.temperature = max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB)
- return REACTING
- return NO_REACTION
+ var/new_heat_capacity = air.heat_capacity()
+ if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
+ air.temperature = max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB)
+ return REACTING
/datum/gas_reaction/proto_nitrate_bz_response
priority_group = PRIORITY_PRE_FORMATION
@@ -824,6 +869,7 @@
var/list/cached_gases = air.gases
var/temperature = air.temperature
var/turf/open/location
+
if(istype(holder,/datum/pipeline)) //Find the tile the reaction is occuring on, or a random part of the network if it's a pipenet.
var/datum/pipeline/pipenet = holder
location = get_turf(pick(pipenet.members))
@@ -842,6 +888,7 @@
cached_gases[/datum/gas/bz][MOLES] -= consumed_amount
energy_released += consumed_amount * 60000
radiation_pulse(location, max_range = min(sqrt(consumed_amount) / 1.5, 20), threshold = 15 * INVERSE(15 + consumed_amount), chance = 50)
+ SET_REACTION_RESULTS(consumed_amount)
for(var/mob/living/carbon/L in location)
L.hallucination += consumed_amount
if(energy_released)
@@ -877,12 +924,14 @@
var produced_amount = min(air.temperature / 34 * (cached_gases[/datum/gas/tritium][MOLES] * cached_gases[/datum/gas/proto_nitrate][MOLES]) / (cached_gases[/datum/gas/tritium][MOLES] + 10 * cached_gases[/datum/gas/proto_nitrate][MOLES]), cached_gases[/datum/gas/tritium][MOLES], cached_gases[/datum/gas/proto_nitrate][MOLES] * INVERSE(0.01))
if(cached_gases[/datum/gas/tritium][MOLES] - produced_amount < 0 || cached_gases[/datum/gas/proto_nitrate][MOLES] - produced_amount * 0.01 < 0)
return NO_REACTION
+
ASSERT_GAS(/datum/gas/hydrogen, air)
cached_gases[/datum/gas/tritium][MOLES] -= produced_amount
cached_gases[/datum/gas/proto_nitrate][MOLES] -= produced_amount * 0.01
cached_gases[/datum/gas/hydrogen][MOLES] += produced_amount
energy_released += produced_amount * 10000
radiation_pulse(location, max_range = min(sqrt(produced_amount) / 1.5, 20), threshold = 15 * INVERSE(15 + produced_amount), chance = 50)
+ SET_REACTION_RESULTS(produced_amount)
if(energy_released)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -906,11 +955,15 @@
var/list/cached_gases = air.gases
var/temperature = air.temperature
var produced_amount = min(5, cached_gases[/datum/gas/hydrogen][MOLES], cached_gases[/datum/gas/proto_nitrate][MOLES])
+
if(cached_gases[/datum/gas/hydrogen][MOLES] - produced_amount < 0)
return NO_REACTION
+
cached_gases[/datum/gas/hydrogen][MOLES] -= produced_amount
cached_gases[/datum/gas/proto_nitrate][MOLES] += produced_amount * 0.5
energy_released = produced_amount * 2500
+ SET_REACTION_RESULTS(produced_amount)
+
if(energy_released)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
@@ -936,19 +989,28 @@
var/old_heat_capacity = air.heat_capacity()
var/list/cached_gases = air.gases
var/temperature = air.temperature
- var produced_amount = min(5, cached_gases[/datum/gas/carbon_dioxide][MOLES], cached_gases[/datum/gas/oxygen][MOLES] * INVERSE(0.5), cached_gases[/datum/gas/tritium][MOLES] * INVERSE(0.01))
+ var/produced_amount = min(5, cached_gases[/datum/gas/carbon_dioxide][MOLES], cached_gases[/datum/gas/oxygen][MOLES] * INVERSE(0.5), cached_gases[/datum/gas/tritium][MOLES] * INVERSE(0.01))
+
if(cached_gases[/datum/gas/carbon_dioxide][MOLES] - produced_amount < 0 || cached_gases[/datum/gas/oxygen][MOLES] - produced_amount * 0.5 < 0 || cached_gases[/datum/gas/tritium][MOLES] - produced_amount * 0.01 < 0)
return NO_REACTION
+
+ ASSERT_GAS(/datum/gas/pluoxium, air)
+ ASSERT_GAS(/datum/gas/hydrogen, air)
+
cached_gases[/datum/gas/carbon_dioxide][MOLES] -= produced_amount
cached_gases[/datum/gas/oxygen][MOLES] -= produced_amount * 0.5
cached_gases[/datum/gas/tritium][MOLES] -= produced_amount * 0.01
- ASSERT_GAS(/datum/gas/pluoxium, air)
+
cached_gases[/datum/gas/pluoxium][MOLES] += produced_amount
- ASSERT_GAS(/datum/gas/hydrogen, air)
cached_gases[/datum/gas/hydrogen][MOLES] += produced_amount * 0.01
energy_released += produced_amount * 250
+
+ SET_REACTION_RESULTS(produced_amount)
+
if(energy_released)
var/new_heat_capacity = air.heat_capacity()
if(new_heat_capacity > MINIMUM_HEAT_CAPACITY)
air.temperature = max((temperature * old_heat_capacity + energy_released) / new_heat_capacity, TCMB)
return REACTING
+
+#undef SET_REACTION_RESULTS
diff --git a/code/modules/experisci/experiment/experiments.dm b/code/modules/experisci/experiment/experiments.dm
index b0d9f2ab035..c3c4e8b4180 100644
--- a/code/modules/experisci/experiment/experiments.dm
+++ b/code/modules/experisci/experiment/experiments.dm
@@ -54,27 +54,86 @@
/obj/effect/decal/cleanable/blood)
total_requirement = 3
-/datum/experiment/explosion/calibration
- name = "Is This Thing On?"
- description = "The engineers from last shift left a notice for us that the doppler array seemed to be malfunctioning. \
- Could you check that it is still working? Any explosion will do!"
- required_light = 1
+/datum/experiment/ordnance/explosive/lowyieldbomb
+ name = "Low-Yield Explosives"
+ description = "Low-yield explosives may prove useful for our asset protection teams. Perform research and publish papers on this field. Explosion from any source is allowed."
+ gain = list(10,15,20)
+ target_amount = list(5,10,20)
+ experiment_proper = TRUE
+ sanitized_misc = FALSE
+ sanitized_reactions = FALSE
+ allow_any_source = TRUE
-/datum/experiment/explosion/maxcap
- name = "Mother of God"
- description = "A recent outbreak of a blood-cult in a nearby sector necessitates the development of a large explosive. \
- Create a large enough explosion to prove your bomb, we'll be watching."
+/datum/experiment/ordnance/explosive/highyieldbomb
+ name = "High-Yield Explosives"
+ description = "Several reactions react very energetically and can be utilized for bigger explosives. Perform research and publish papers on this field. Any gas reaction is allowed."
+ gain = list(10,50,100)
+ target_amount = list(50,100,300)
+ experiment_proper = TRUE
+ sanitized_misc = FALSE
+ sanitized_reactions = FALSE
-/datum/experiment/explosion/medium
- name = "Explosive Ordinance Experiment"
- description = "Alright, can we really call ourselves professionals if we can't make shit blow up?"
- required_heavy = 2
- required_light = 6
+/datum/experiment/ordnance/explosive/hydrogenbomb
+ name = "Hydrogen Explosives"
+ description = "Combustion of Hydrogen and it's derivatives can be very powerful. Perform research and publish papers on this field. Only the specified gas reactions are allowed."
+ gain = list(15,40,60)
+ target_amount = list(50,75,150)
+ experiment_proper = TRUE
+ sanitized_misc = TRUE
+ sanitized_reactions = TRUE
+ require_all = FALSE
+ required_reactions = list(/datum/gas_reaction/h2fire, /datum/gas_reaction/tritfire)
-/datum/experiment/explosion/maxcap/New()
- required_devastation = GLOB.MAX_EX_DEVESTATION_RANGE
- required_heavy = GLOB.MAX_EX_HEAVY_RANGE
- required_light = GLOB.MAX_EX_LIGHT_RANGE
+/datum/experiment/ordnance/explosive/nobliumbomb
+ name = "Noblium Explosives"
+ description = "The formation of Hyper-Noblium is very energetic and can be harnessed for explosives. Perform research and publish papers on this field. Only the specified gas reaction is allowed."
+ gain = list(15,60,120)
+ target_amount = list(50,100,300)
+ experiment_proper = TRUE
+ sanitized_misc = TRUE
+ sanitized_reactions = TRUE
+ required_reactions = list(/datum/gas_reaction/nobliumformation)
+
+/datum/experiment/ordnance/explosive/pressurebomb
+ name = "Reactionless Explosives"
+ description = "Gases with high specific heat can heat up those with a low one and produce a lot of pressure. Perform research and publish papers on this field. No gas reactions are allowed."
+ gain = list(10,50,100)
+ target_amount = list(20,75,200)
+ experiment_proper = TRUE
+ sanitized_misc = FALSE
+ sanitized_reactions = TRUE
+
+/datum/experiment/ordnance/gaseous/nitrium
+ name = "Nitrium Gas Shells"
+ description = "The delivery of Nitrium gas into an area of operation might prove useful. Perform research and publish papers on this field."
+ gain = list(20,60,120)
+ target_amount = list(20,120,500)
+ experiment_proper = TRUE
+ required_gas = /datum/gas/nitrium
+
+/datum/experiment/ordnance/gaseous/bz
+ name = "BZ Gas Shells"
+ description = "The delivery of BZ gas into an area of operation might prove useful. Perform research and publish papers on this field."
+ gain = list(25,50)
+ target_amount = list(200,600)
+ experiment_proper = TRUE
+ required_gas = /datum/gas/bz
+
+/datum/experiment/ordnance/gaseous/noblium
+ name = "Noblium Gas Shells"
+ description = "The delivery of Noblium gas into an area of operation might prove useful. Perform research and publish papers on this field."
+ gain = list(10,40,80)
+ target_amount = list(15,55,250)
+ experiment_proper = TRUE
+ required_gas = /datum/gas/hypernoblium
+
+/datum/experiment/ordnance/gaseous/halon
+ name = "Halon Gas Shells"
+ description = "The delivery of Halon gas into an area of operation might prove useful. Perform research and publish papers on this field."
+ gain = list(10,30,60)
+ target_amount = list(15,55,250)
+ experiment_proper = TRUE
+ required_gas = /datum/gas/halon
/datum/experiment/scanning/random/material/meat
name = "Biological Material Scanning Experiment"
diff --git a/code/modules/experisci/experiment/handlers/experiment_handler.dm b/code/modules/experisci/experiment/handlers/experiment_handler.dm
index cc177a79eb4..4909578fede 100644
--- a/code/modules/experisci/experiment/handlers/experiment_handler.dm
+++ b/code/modules/experisci/experiment/handlers/experiment_handler.dm
@@ -52,8 +52,6 @@
if(isitem(parent))
RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, .proc/try_run_handheld_experiment)
RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, .proc/ignored_handheld_experiment_attempt)
- if(istype(parent, /obj/machinery/doppler_array))
- RegisterSignal(parent, COMSIG_DOPPLER_ARRAY_EXPLOSION_DETECTED, .proc/try_run_doppler_experiment)
if(istype(parent, /obj/machinery/destructive_scanner))
RegisterSignal(parent, COMSIG_MACHINERY_DESTRUCTIVE_SCAN, .proc/try_run_destructive_experiment)
if(istype(parent, /obj/machinery/computer/operating))
@@ -161,19 +159,6 @@
else
playsound(src, 'sound/machines/buzz-sigh.ogg', 25)
our_scanner.say("The scan did not result in anything.")
-/**
- * Hooks on successful explosions on the doppler array this is attached to
- */
-/datum/component/experiment_handler/proc/try_run_doppler_experiment(datum/source, turf/epicenter, devastation_range,
- heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range
-)
- SIGNAL_HANDLER
- var/atom/movable/our_array = parent
- if(action_experiment(source, devastation_range, heavy_impact_range, light_impact_range))
- playsound(src, 'sound/machines/ping.ogg', 25)
- else
- playsound(src, 'sound/machines/buzz-sigh.ogg', 25)
- our_array.say("Insufficient explosion to contribute to current experiment.")
/// Hooks on a successful dissection experiment
/datum/component/experiment_handler/proc/try_run_dissection_experiment(obj/source, mob/living/target)
diff --git a/code/modules/experisci/experiment/types/explosive.dm b/code/modules/experisci/experiment/types/explosive.dm
deleted file mode 100644
index 288e4d1f6d9..00000000000
--- a/code/modules/experisci/experiment/types/explosive.dm
+++ /dev/null
@@ -1,39 +0,0 @@
-/datum/experiment/explosion
- name = "Explosive Experiment"
- description = "An experiment requiring an explosion to progress"
- exp_tag = "Explosion"
- performance_hint = "Perform explosive experiments using the research doppler array in the ordnance lab."
- /// The required devastation range to complete the experiment
- var/required_devastation = 0
- /// The required heavy impact range to complete the experiment
- var/required_heavy = 0
- /// The required light impact range to complete the experiment
- var/required_light = 0
- /// The last measured devastation range
- var/last_devastation
- /// The last measured heavy range
- var/last_heavy
- /// The last measured light range
- var/last_light
-
-/datum/experiment/explosion/is_complete()
- return (!required_devastation || required_devastation <= last_devastation) \
- && (!required_heavy || required_heavy <= last_heavy) \
- && (!required_light || required_light <= last_light)
-
-/datum/experiment/explosion/check_progress()
- var/status_message = "You must record an explosion with ranges of at least \
- [required_devastation] devastation, [required_heavy] heavy, and [required_light] light."
- if (last_devastation || last_heavy || last_light)
- status_message += " The last attempt had ranges of [last_devastation]D/[last_heavy]H/[last_light]L."
- . += EXPERIMENT_PROG_BOOL(status_message, is_complete())
-
-/datum/experiment/explosion/perform_experiment_actions(datum/component/experiment_handler/experiment_handler, devastation, heavy, light)
- last_devastation = devastation
- last_heavy = heavy
- last_light = light
- return is_complete()
-
-/datum/experiment/explosion/actionable(datum/component/experiment_handler/experiment_handler, devastation, heavy, light)
- return !is_complete()
-
diff --git a/code/modules/experisci/experiment/types/ordnance.dm b/code/modules/experisci/experiment/types/ordnance.dm
new file mode 100644
index 00000000000..944fa5a757b
--- /dev/null
+++ b/code/modules/experisci/experiment/types/ordnance.dm
@@ -0,0 +1,48 @@
+/**
+ * A ordnance experiment datum. What gives the science in the first place.
+ * One is instantiated by the techweb, another one is also kept on SSresearch for the briefing.
+ * A disk should contain several of these in a list. Only one should get picked for the final paper.
+ */
+/datum/experiment/ordnance
+ name = "Toxin Research"
+ description = "An experiment conducted in the ordnance subdepartment."
+ exp_tag = "ordnance"
+ performance_hint = "Perform or purchase research experiments in the ordnance lab."
+ /// Lookup experiments are initialized using subtypes,
+ /// this lets us ignore the ones made for subtyping.
+ var/experiment_proper = FALSE
+ /// Projected gain from an experiment. In list form, indexed = tier.
+ var/list/gain
+ /// The variable that influences the point equation; Determines when you get the most bang for your buck
+ /// More rigorously, this is the middle of the derivative bell curve.
+ /// In list form, indexed = tier.
+ var/list/target_amount
+
+/datum/experiment/ordnance/is_complete()
+ return completed
+
+/datum/experiment/ordnance/check_progress()
+ var/status_message = "You must publish or purchase a paper on [name]"
+ . += EXPERIMENT_PROG_BOOL(status_message, is_complete())
+
+/datum/experiment/ordnance/actionable(datum/component/experiment_handler/experiment_handler)
+ return !is_complete()
+
+/datum/experiment/ordnance/explosive
+ /// Typepath to gas reactions that we want. Empty if we do not require any at all or don't want any.
+ var/list/required_reactions
+ /// Whether we only accept explosions with reactions that we set in src.required_reactions or do we allow other reactions in.
+ var/sanitized_reactions = FALSE
+ /// Whether we want everything in required_reactions to be fulfilled, or only one is needed.
+ var/require_all = TRUE
+ /// Whether we want a clear misc or not.
+ var/sanitized_misc = TRUE
+ /**
+ * Whether we allow bombs without properly filled reaction_results or not.
+ * Setting this to true means the experiment will always pass the cause and effect check.
+ */
+ var/allow_any_source = FALSE
+
+/datum/experiment/ordnance/gaseous
+ /// Typepath to the gas we require for the experiment.
+ var/required_gas
diff --git a/code/modules/modular_computers/computers/machinery/console_presets.dm b/code/modules/modular_computers/computers/machinery/console_presets.dm
index 0cb2a653755..e2e79eed7d0 100644
--- a/code/modules/modular_computers/computers/machinery/console_presets.dm
+++ b/code/modules/modular_computers/computers/machinery/console_presets.dm
@@ -52,7 +52,7 @@
hard_drive.store_file(new/datum/computer_file/program/chatclient())
hard_drive.store_file(new/datum/computer_file/program/aidiag())
hard_drive.store_file(new/datum/computer_file/program/robocontrol())
-
+ hard_drive.store_file(new/datum/computer_file/program/scipaper_program())
// ===== COMMAND CONSOLE =====
/obj/machinery/modular_computer/console/preset/command
diff --git a/code/modules/modular_computers/file_system/data.dm b/code/modules/modular_computers/file_system/data.dm
index e0404e787fb..a67f8c9e02e 100644
--- a/code/modules/modular_computers/file_system/data.dm
+++ b/code/modules/modular_computers/file_system/data.dm
@@ -1,20 +1,26 @@
-// /data/ files store data in string format.
-// They don't contain other logic for now.
+/// Data files. Doesn't really hold many important functionalities, here for organization.
/datum/computer_file/data
- var/stored_data = "" // Stored data in string format.
+ /// Whether the user will be reminded that the file probably shouldn't be edited.
+ var/do_not_edit = FALSE
filetype = "DAT"
- var/block_size = 250
- var/do_not_edit = 0 // Whether the user will be reminded that the file probably shouldn't be edited.
-/datum/computer_file/data/clone()
- var/datum/computer_file/data/temp = ..()
- temp.stored_data = stored_data
+// /data/text files store data in string format.
+// They don't contain other logic for now.
+/datum/computer_file/data/text
+ filetype = "TXT"
+ /// Stored data in string format.
+ var/stored_text = ""
+ var/block_size = 250
+
+/datum/computer_file/data/text/clone()
+ var/datum/computer_file/data/text/temp = ..()
+ temp.stored_text = stored_text
return temp
// Calculates file size from amount of characters in saved string
-/datum/computer_file/data/proc/calculate_size()
- size = max(1, round(length(stored_data) / block_size))
+/datum/computer_file/data/text/proc/calculate_size()
+ size = max(1, round(length(stored_text) / block_size))
-/datum/computer_file/data/logfile
+/datum/computer_file/data/text/logfile
filetype = "LOG"
diff --git a/code/modules/modular_computers/file_system/programs/airestorer.dm b/code/modules/modular_computers/file_system/programs/airestorer.dm
index 2c24c37a04a..e3b4f2ac742 100644
--- a/code/modules/modular_computers/file_system/programs/airestorer.dm
+++ b/code/modules/modular_computers/file_system/programs/airestorer.dm
@@ -1,7 +1,7 @@
/datum/computer_file/program/aidiag
filename = "aidiag"
filedesc = "NT FRK"
- category = PROGRAM_CATEGORY_ROBO
+ category = PROGRAM_CATEGORY_SCI
program_icon_state = "generic"
extended_desc = "Firmware Restoration Kit, capable of reconstructing damaged AI systems. Requires direct AI connection via intellicard slot."
size = 12
diff --git a/code/modules/modular_computers/file_system/programs/borg_monitor.dm b/code/modules/modular_computers/file_system/programs/borg_monitor.dm
index cebde040f02..9d9bdcc0321 100644
--- a/code/modules/modular_computers/file_system/programs/borg_monitor.dm
+++ b/code/modules/modular_computers/file_system/programs/borg_monitor.dm
@@ -1,7 +1,7 @@
/datum/computer_file/program/borg_monitor
filename = "siliconnect"
filedesc = "SiliConnect"
- category = PROGRAM_CATEGORY_ROBO
+ category = PROGRAM_CATEGORY_SCI
ui_header = "borg_mon.gif"
program_icon_state = "generic"
extended_desc = "This program allows for remote monitoring of station cyborgs."
@@ -162,7 +162,7 @@
/datum/computer_file/program/borg_monitor/syndicate
filename = "roboverlord"
filedesc = "Roboverlord"
- category = PROGRAM_CATEGORY_ROBO
+ category = PROGRAM_CATEGORY_SCI
ui_header = "borg_mon.gif"
program_icon_state = "generic"
extended_desc = "This program allows for remote monitoring of mission-assigned cyborgs."
diff --git a/code/modules/modular_computers/file_system/programs/ntdownloader.dm b/code/modules/modular_computers/file_system/programs/ntdownloader.dm
index 15dd3278031..a7010efd78a 100644
--- a/code/modules/modular_computers/file_system/programs/ntdownloader.dm
+++ b/code/modules/modular_computers/file_system/programs/ntdownloader.dm
@@ -24,7 +24,7 @@
var/list/show_categories = list(
PROGRAM_CATEGORY_CREW,
PROGRAM_CATEGORY_ENGI,
- PROGRAM_CATEGORY_ROBO,
+ PROGRAM_CATEGORY_SCI,
PROGRAM_CATEGORY_SUPL,
PROGRAM_CATEGORY_MISC,
)
diff --git a/code/modules/modular_computers/file_system/programs/ntnrc_client.dm b/code/modules/modular_computers/file_system/programs/ntnrc_client.dm
index 20949b0dffd..60a8b20a8f1 100644
--- a/code/modules/modular_computers/file_system/programs/ntnrc_client.dm
+++ b/code/modules/modular_computers/file_system/programs/ntnrc_client.dm
@@ -116,13 +116,13 @@
var/logname = stripped_input(params["log_name"])
if(!logname)
return
- var/datum/computer_file/data/logfile = new /datum/computer_file/data/logfile()
+ var/datum/computer_file/data/text/logfile = new()
// Now we will generate HTML-compliant file that can actually be viewed/printed.
logfile.filename = logname
- logfile.stored_data = "\[b\]Logfile dump from NTNRC channel [channel.title]\[/b\]\[BR\]"
+ logfile.stored_text = "\[b\]Logfile dump from NTNRC channel [channel.title]\[/b\]\[BR\]"
for(var/logstring in channel.messages)
- logfile.stored_data = "[logfile.stored_data][logstring]\[BR\]"
- logfile.stored_data = "[logfile.stored_data]\[b\]Logfile dump completed.\[/b\]"
+ logfile.stored_text = "[logfile.stored_text][logstring]\[BR\]"
+ logfile.stored_text = "[logfile.stored_text]\[b\]Logfile dump completed.\[/b\]"
logfile.calculate_size()
var/obj/item/computer_hardware/hard_drive/hard_drive = computer.all_components[MC_HDD]
if(!computer || !hard_drive || !hard_drive.store_file(logfile))
diff --git a/code/modules/modular_computers/file_system/programs/robocontrol.dm b/code/modules/modular_computers/file_system/programs/robocontrol.dm
index 5d2ec5d422f..2ea4acb06f8 100644
--- a/code/modules/modular_computers/file_system/programs/robocontrol.dm
+++ b/code/modules/modular_computers/file_system/programs/robocontrol.dm
@@ -2,7 +2,7 @@
/datum/computer_file/program/robocontrol
filename = "botkeeper"
filedesc = "BotKeeper"
- category = PROGRAM_CATEGORY_ROBO
+ category = PROGRAM_CATEGORY_SCI
program_icon_state = "robot"
extended_desc = "A remote controller used for giving basic commands to non-sentient robots."
transfer_access = null
diff --git a/code/modules/modular_computers/file_system/programs/robotact.dm b/code/modules/modular_computers/file_system/programs/robotact.dm
index 339aa01649b..3e5f83410f2 100644
--- a/code/modules/modular_computers/file_system/programs/robotact.dm
+++ b/code/modules/modular_computers/file_system/programs/robotact.dm
@@ -1,7 +1,7 @@
/datum/computer_file/program/robotact
filename = "robotact"
filedesc = "RoboTact"
- category = PROGRAM_CATEGORY_ROBO
+ category = PROGRAM_CATEGORY_SCI
extended_desc = "A built-in app for cyborg self-management and diagnostics."
ui_header = "robotact.gif" //DEBUG -- new icon before PR
program_icon_state = "command"
diff --git a/code/modules/modular_computers/file_system/programs/techweb.dm b/code/modules/modular_computers/file_system/programs/techweb.dm
index 957498f3d10..097bc881040 100644
--- a/code/modules/modular_computers/file_system/programs/techweb.dm
+++ b/code/modules/modular_computers/file_system/programs/techweb.dm
@@ -1,6 +1,7 @@
/datum/computer_file/program/science
filename = "experi_track"
filedesc = "Nanotrasen Science Hub"
+ category = PROGRAM_CATEGORY_SCI
program_icon_state = "research"
extended_desc = "Connect to the internal science server in order to assist in station research efforts."
requires_ntnet = TRUE
diff --git a/code/modules/modular_computers/hardware/hard_drive.dm b/code/modules/modular_computers/hardware/hard_drive.dm
index 4dc40ef4466..d499fca4443 100644
--- a/code/modules/modular_computers/hardware/hard_drive.dm
+++ b/code/modules/modular_computers/hardware/hard_drive.dm
@@ -48,9 +48,13 @@
if(F in stored_files)
return FALSE
+ SEND_SIGNAL(F, COMSIG_MODULAR_COMPUTER_FILE_ADDING)
+
F.holder = src
stored_files.Add(F)
recalculate_size()
+
+ SEND_SIGNAL(F, COMSIG_MODULAR_COMPUTER_FILE_ADDED)
return TRUE
// Use this proc to remove file from the drive. Returns 1 on success and 0 on failure. Contains necessary sanity checks.
@@ -65,8 +69,10 @@
return FALSE
if(F in stored_files)
+ SEND_SIGNAL(F, COMSIG_MODULAR_COMPUTER_FILE_DELETING)
stored_files -= F
recalculate_size()
+ SEND_SIGNAL(F, COMSIG_MODULAR_COMPUTER_FILE_DELETED)
return TRUE
else
return FALSE
diff --git a/code/modules/power/power.dm b/code/modules/power/power.dm
index 1c4b621da8c..dc196871d0d 100644
--- a/code/modules/power/power.dm
+++ b/code/modules/power/power.dm
@@ -167,16 +167,17 @@
if(machine_stat & BROKEN)
return
+ var/initial_stat = machine_stat
if(powered(power_channel))
- if(machine_stat & NOPOWER)
+ set_machine_stat(machine_stat & ~NOPOWER)
+ if(initial_stat & NOPOWER)
SEND_SIGNAL(src, COMSIG_MACHINERY_POWER_RESTORED)
. = TRUE
- set_machine_stat(machine_stat & ~NOPOWER)
else
- if(!(machine_stat & NOPOWER))
+ set_machine_stat(machine_stat | NOPOWER)
+ if(!(initial_stat & NOPOWER))
SEND_SIGNAL(src, COMSIG_MACHINERY_POWER_LOST)
. = TRUE
- set_machine_stat(machine_stat | NOPOWER)
update_appearance()
// connect the machine to a powernet if a node cable or a terminal is present on the turf
diff --git a/code/modules/research/anomaly/anomaly_refinery.dm b/code/modules/research/anomaly/anomaly_refinery.dm
new file mode 100644
index 00000000000..f29beef1846
--- /dev/null
+++ b/code/modules/research/anomaly/anomaly_refinery.dm
@@ -0,0 +1,314 @@
+#define MAX_RADIUS_REQUIRED 20 //maxcap
+#define MIN_RADIUS_REQUIRED 4 //1, 2, 4
+/// How long the compression test can last before the machine just gives up and ejects the items.
+#define COMPRESSION_TEST_TIME (SSOBJ_DT SECONDS * 5)
+
+/**
+ * # Explosive compressor machines
+ *
+ * The explosive compressor machine used in anomaly core production.
+ *
+ * Uses the standard ordnance/tank explosion scaling to compress raw anomaly cores into completed ones. The required explosion radius increases as more cores of that type are created.
+ */
+/obj/machinery/research/anomaly_refinery
+ name = "anomaly refinery"
+ desc = "An advanced machine capable of implosion-compressing raw anomaly cores into finished artifacts. Also equipped with state of the art bomb prediction software."
+ circuit = /obj/item/circuitboard/machine/anomaly_refinery
+ icon = 'icons/obj/machines/research.dmi'
+ base_icon_state = "explosive_compressor"
+ icon_state = "explosive_compressor"
+ density = TRUE
+
+ /// The raw core inserted in the machine.
+ var/obj/item/raw_anomaly_core/inserted_core
+ /// The TTV inserted in the machine.
+ var/obj/item/transfer_valve/inserted_bomb
+ /// The timer that lets us timeout the test.
+ var/datum/timedevent/timeout_timer
+ /// Whether we are currently active a bomb and core.
+ var/active = FALSE
+ /// The message produced by the explosive compressor at the end of the compression test.
+ var/test_status = null
+ /// Determines which tank will be the merge_gases target (destroyed upon testing).
+ var/obj/item/tank/tank_to_target
+
+ // These vars are used for the explosion simulation and doesn't affect the core detonation.
+ /// Combined result of the first two tanks. Exists only in our machine.
+ var/datum/gas_mixture/combined_gasmix
+ /// Here for the UI, tracks the amounts of reaction that has occured. 1 means valve opened but not reacted.
+ var/reaction_increment = 0
+
+/obj/machinery/research/anomaly_refinery/Initialize(mapload)
+ . = ..()
+ RegisterSignal(src, COMSIG_ATOM_INTERNAL_EXPLOSION, .proc/check_test)
+
+/obj/machinery/research/anomaly_refinery/assume_air(datum/gas_mixture/giver)
+ return null // Required to make the TTV not vent directly into the air.
+
+/**
+ * Determines how much explosive power (last value, so light impact theoretical radius) is required to make a certain anomaly type.
+ *
+ * Returns null if the max amount has already been reached.
+ *
+ * Arguments:
+ * * anomaly_type - anomaly type define
+ */
+/obj/machinery/research/anomaly_refinery/proc/get_required_radius(anomaly_type)
+ var/already_made = SSresearch.created_anomaly_types[anomaly_type]
+ var/hard_limit = SSresearch.anomaly_hard_limit_by_type[anomaly_type]
+ if(already_made >= hard_limit)
+ return //return null
+ // my crappy autoscale formula
+ // linear scaling.
+ var/radius_span = MAX_RADIUS_REQUIRED - MIN_RADIUS_REQUIRED
+ var/radius_increase_per_core = radius_span / hard_limit
+ var/radius = clamp(round(MIN_RADIUS_REQUIRED + radius_increase_per_core * already_made, 1), MIN_RADIUS_REQUIRED, MAX_RADIUS_REQUIRED)
+ return radius
+
+/obj/machinery/research/anomaly_refinery/attackby(obj/item/tool, mob/living/user, params)
+ if(active)
+ to_chat(user, span_warning("You can't insert [tool] into [src] while [p_theyre()] currently active."))
+ return
+ if(istype(tool, /obj/item/raw_anomaly_core))
+ if(inserted_core)
+ to_chat(user, span_warning("There is already a core in [src]."))
+ return
+ if(!user.transferItemToLoc(tool, src))
+ to_chat(user, span_warning("[tool] is stuck to your hand."))
+ return
+ var/obj/item/raw_anomaly_core/raw_core = tool
+ if(!get_required_radius(raw_core.anomaly_type))
+ say("Unfortunately, due to diminishing supplies of condensed anomalous matter, [raw_core] and any cores of its type are no longer of a sufficient quality level to be compressed into a working core.")
+ inserted_core = raw_core
+ to_chat(user, span_notice("You insert [raw_core] into [src]."))
+ return
+ if(istype(tool, /obj/item/transfer_valve))
+ if(inserted_bomb)
+ to_chat(user, span_warning("There is already a bomb in [src]."))
+ return
+ var/obj/item/transfer_valve/valve = tool
+ if(!valve.ready())
+ to_chat(user, span_warning("[valve] is incomplete."))
+ return
+ if(!user.transferItemToLoc(tool, src))
+ to_chat(user, span_warning("[tool] is stuck to your hand."))
+ return
+ inserted_bomb = tool
+ tank_to_target = inserted_bomb.tank_two
+ to_chat(user, span_notice("You insert [tool] into [src]"))
+ return
+ update_appearance()
+ return ..()
+
+/obj/machinery/research/anomaly_refinery/wrench_act(mob/living/user, obj/item/tool)
+ if(!default_unfasten_wrench(user, tool))
+ return FALSE
+ return TRUE
+
+/obj/machinery/research/anomaly_refinery/screwdriver_act(mob/living/user, obj/item/tool)
+ if(!default_deconstruction_screwdriver(user, "[base_icon_state]-off", "[base_icon_state]", tool))
+ return FALSE
+ update_appearance()
+ return TRUE
+
+/obj/machinery/research/anomaly_refinery/crowbar_act(mob/living/user, obj/item/tool)
+ if(!default_deconstruction_crowbar(tool))
+ return FALSE
+ return TRUE
+
+/**
+ * Starts a compression test.
+ */
+/obj/machinery/research/anomaly_refinery/proc/start_test()
+ if(!istype(inserted_core) || !istype(inserted_bomb))
+ end_test("ERROR: Missing equpment. Items ejected.")
+ return
+
+ if(!inserted_bomb?.tank_one || !inserted_bomb?.tank_two || !(tank_to_target == inserted_bomb?.tank_one || tank_to_target == inserted_bomb?.tank_two))
+ end_test("ERROR: Transfer valve malfunctioning. Items ejected.")
+ return
+
+ say("Beginning compression test. Opening transfer valve.")
+ active = TRUE
+ test_status = null
+ inserted_bomb.toggle_valve(tank_to_target)
+ tank_to_target = null
+ timeout_timer = addtimer(CALLBACK(src, .proc/timeout_test), COMPRESSION_TEST_TIME, TIMER_STOPPABLE | TIMER_UNIQUE | TIMER_NO_HASH_WAIT)
+ return
+
+/**
+ * Ends a compression test.
+ *
+ * Arguments:
+ * - message: A message for the compressor to say when the test ends.
+ */
+/obj/machinery/research/anomaly_refinery/proc/end_test(message)
+ active = FALSE
+ if(inserted_core)
+ eject_core()
+ if(inserted_bomb)
+ eject_bomb()
+ if(timeout_timer)
+ QDEL_NULL(timeout_timer)
+ if(message)
+ say(message)
+ return
+
+/**
+ * Checks whether an internal explosion was sufficient to compress the core.
+ */
+/obj/machinery/research/anomaly_refinery/proc/check_test(atom/source, list/arguments)
+ SIGNAL_HANDLER
+ if(!inserted_core)
+ test_status = "ERROR: No core present during detonation."
+ return COMSIG_CANCEL_EXPLOSION
+
+ var/heavy = arguments[EXARG_KEY_DEV_RANGE]
+ var/medium = arguments[EXARG_KEY_HEAVY_RANGE]
+ var/light = arguments[EXARG_KEY_LIGHT_RANGE]
+ var/explosion_range = max(heavy, medium, light, 0)
+ var/required_range = get_required_radius(inserted_core.anomaly_type)
+ var/turf/location = get_turf(src)
+
+ var/cap_multiplier = SSmapping.level_trait(location.z, ZTRAIT_BOMBCAP_MULTIPLIER)
+ if(isnull(cap_multiplier))
+ cap_multiplier = 1
+ var/capped_heavy = min(GLOB.MAX_EX_DEVESTATION_RANGE * cap_multiplier, heavy)
+ var/capped_medium = min(GLOB.MAX_EX_HEAVY_RANGE * cap_multiplier, medium)
+ SSexplosions.shake_the_room(location, explosion_range, (capped_heavy * 15) + (capped_medium * 20), capped_heavy, capped_medium)
+
+ if(explosion_range < required_range)
+ test_status = "Resultant detonation failed to produce enough implosive power to compress [inserted_core]. Items ejected."
+ return COMSIG_CANCEL_EXPLOSION
+
+ if(test_status)
+ return COMSIG_CANCEL_EXPLOSION
+ inserted_core = inserted_core.create_core(src, TRUE, TRUE)
+ test_status = "Success. Resultant detonation has theoretical range of [explosion_range]. Required radius was [required_range]. Core production complete."
+ return COMSIG_CANCEL_EXPLOSION
+
+/**
+ * Handles timing out the test after a while.
+ */
+/obj/machinery/research/anomaly_refinery/proc/timeout_test()
+ timeout_timer = null
+ if(!test_status)
+ test_status = "Transfer valve resulted in negligible explosive power. Items ejected."
+ end_test(test_status)
+
+/// This is not the real valve opening process. This is the simulated one used for displaying reactions.
+/obj/machinery/research/anomaly_refinery/proc/simulate_valve()
+ if(!inserted_bomb?.tank_one || !inserted_bomb?.tank_two)
+ eject_bomb()
+ return FALSE
+
+ if(reaction_increment == 0)
+ var/datum/gas_mixture/first_gasmix = inserted_bomb.tank_one.return_air()
+ var/datum/gas_mixture/second_gasmix = inserted_bomb.tank_two.return_air()
+
+ combined_gasmix = new(70)
+ combined_gasmix.volume = first_gasmix.volume + second_gasmix.volume
+ combined_gasmix.merge(first_gasmix.copy())
+ combined_gasmix.merge(second_gasmix.copy())
+ else
+ combined_gasmix.react()
+
+ reaction_increment += 1
+
+/// We dont allow incomplete valves to go in but do code in checks for incomplete valves. Just in case.
+/obj/machinery/research/anomaly_refinery/proc/eject_bomb(mob/user)
+ if(!inserted_bomb || active)
+ return
+ if(user)
+ user.put_in_hands(inserted_bomb)
+ to_chat(user, span_notice("You remove [inserted_bomb] from [src]."))
+ else
+ inserted_bomb.forceMove(drop_location())
+ combined_gasmix = null
+ reaction_increment = 0
+
+/obj/machinery/research/anomaly_refinery/proc/eject_core(mob/user)
+ if(!inserted_core || active)
+ return
+ if(user)
+ user.put_in_hands(inserted_core)
+ to_chat(user, span_notice("You remove [inserted_core] from [src]."))
+ else
+ inserted_core.forceMove(drop_location())
+
+/// We rely on exited to clear references.
+/obj/machinery/research/anomaly_refinery/Exited(atom/movable/gone, direction)
+ if(gone == inserted_bomb)
+ inserted_bomb = null
+ tank_to_target = null
+ if(gone == inserted_core)
+ inserted_core = null
+ . = ..()
+
+/obj/machinery/research/anomaly_refinery/proc/swap_target()
+ if(!inserted_bomb?.tank_one || !inserted_bomb?.tank_two)
+ eject_bomb()
+ return FALSE
+ tank_to_target = (tank_to_target == inserted_bomb.tank_one) ? inserted_bomb.tank_two : inserted_bomb.tank_one
+
+/obj/machinery/research/anomaly_refinery/on_deconstruction()
+ eject_bomb()
+ eject_core()
+ return ..()
+
+/obj/machinery/research/anomaly_refinery/Destroy()
+ inserted_bomb = null
+ inserted_core = null
+ combined_gasmix = null
+ return ..()
+
+/obj/machinery/research/anomaly_refinery/ui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "AnomalyRefinery")
+ ui.open()
+
+/obj/machinery/research/anomaly_refinery/ui_act(action, list/params)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("react")
+ simulate_valve()
+ if("eject_bomb")
+ eject_bomb(usr)
+ if("eject_core")
+ eject_core(usr)
+ if("start_implosion")
+ start_test()
+ if("swap")
+ swap_target()
+
+/obj/machinery/research/anomaly_refinery/ui_data(mob/user)
+ var/list/data = list()
+ var/list/parsed_gasmixes = list()
+ var/obj/item/tank/other_tank
+
+ if(inserted_bomb?.tank_one && inserted_bomb?.tank_two)
+ other_tank = inserted_bomb.tank_one == tank_to_target ? inserted_bomb.tank_two : inserted_bomb.tank_one
+
+ parsed_gasmixes += list(gas_mixture_parser(tank_to_target?.return_air(), tank_to_target?.name))
+ parsed_gasmixes += list(gas_mixture_parser(other_tank?.return_air(), other_tank?.name))
+ parsed_gasmixes += list(gas_mixture_parser(combined_gasmix, "Combined Gasmix"))
+
+ data["gasList"] = parsed_gasmixes
+
+ data["valvePresent"] = inserted_bomb ? TRUE : FALSE
+ data["valveReady"] = (inserted_bomb?.tank_one && inserted_bomb?.tank_two) ? TRUE : FALSE
+ data["reactionIncrement"] = reaction_increment
+
+ data["core"] = inserted_core ? inserted_core.name : FALSE
+ data["requiredRadius"] = inserted_core ? get_required_radius(inserted_core.anomaly_type) : null
+
+ data["active"] = active
+
+ return data
+
+#undef MAX_RADIUS_REQUIRED
+#undef MIN_RADIUS_REQUIRED
diff --git a/code/modules/research/anomaly/explosive_compressor.dm b/code/modules/research/anomaly/explosive_compressor.dm
deleted file mode 100644
index f7c81c1630b..00000000000
--- a/code/modules/research/anomaly/explosive_compressor.dm
+++ /dev/null
@@ -1,221 +0,0 @@
-#define MAX_RADIUS_REQUIRED 20 //maxcap
-#define MIN_RADIUS_REQUIRED 4 //1, 2, 4
-/// How long the compression test can last before the machine just gives up and ejects the items.
-#define COMPRESSION_TEST_TIME (SSOBJ_DT SECONDS * 5)
-
-/**
- * # Explosive compressor machines
- *
- * The explosive compressor machine used in anomaly core production.
- *
- * Uses the standard ordnance/tank explosion scaling to compress raw anomaly cores into completed ones. The required explosion radius increases as more cores of that type are created.
- */
-/obj/machinery/research/explosive_compressor
- name = "implosion compressor"
- desc = "An advanced machine capable of implosion-compressing raw anomaly cores into finished artifacts."
- icon = 'icons/obj/machines/research.dmi'
- icon_state = "explosive_compressor"
- density = TRUE
-
- /// The raw core inserted in the machine.
- var/obj/item/raw_anomaly_core/inserted_core
- /// The TTV inserted in the machine.
- var/obj/item/transfer_valve/inserted_bomb
- /// The timer that lets us timeout the test.
- var/datum/timedevent/timeout_timer
- /// Whether we are currently testing a bomb and core.
- var/testing = FALSE
- /// The message produced by the explosive compressor at the end of the compression test.
- var/test_status = null
- /// The last time we did say_requirements(), because someone will inevitably click spam this.
- var/last_requirements_say = 0
-
-/obj/machinery/research/explosive_compressor/Initialize(mapload)
- . = ..()
- RegisterSignal(src, COMSIG_ATOM_INTERNAL_EXPLOSION, .proc/check_test)
-
-/obj/machinery/research/explosive_compressor/Destroy()
- UnregisterSignal(src, COMSIG_ATOM_INTERNAL_EXPLOSION)
- return ..()
-
-/obj/machinery/research/explosive_compressor/examine(mob/user)
- . = ..()
- . += span_notice("Ctrl-Click to remove an inserted core.")
- . += span_notice("Click with an empty hand to gather information about the required radius of an inserted core. Insert a ready TTV to start the implosion process if a core is inserted.")
-
-/obj/machinery/research/explosive_compressor/assume_air(datum/gas_mixture/giver)
- qdel(giver)
- return null // Required to make the TTV not vent directly into the air.
-
-/obj/machinery/research/explosive_compressor/attack_hand(mob/living/user, list/modifiers)
- . = ..()
- if(.)
- return
- if(!inserted_core)
- to_chat(user, span_warning("There is no core inserted."))
- return
- if(last_requirements_say + 3 SECONDS > world.time)
- return
- last_requirements_say = world.time
- say_requirements(inserted_core)
-
-/obj/machinery/research/explosive_compressor/CtrlClick(mob/living/user)
- . = ..()
- if(!istype(user) || !user.Adjacent(src) || !(user.mobility_flags & MOBILITY_USE))
- return
- if(!inserted_core)
- to_chat(user, span_warning("There is no core inserted."))
- return
- if(testing)
- to_chat(user, span_warning("You can't remove [inserted_core] from [src] while [p_theyre()] in testing mode."))
- return
- inserted_core.forceMove(get_turf(user))
- to_chat(user, span_notice("You remove [inserted_core] from [src]."))
- user.put_in_hands(inserted_core)
- inserted_core = null
-
-/**
- * Says (no, literally) the data of required explosive power for a certain anomaly type.
- */
-/obj/machinery/research/explosive_compressor/proc/say_requirements(obj/item/raw_anomaly_core/core)
- var/required = get_required_radius(core.anomaly_type)
- if(isnull(required))
- say("Unfortunately, due to diminishing supplies of condensed anomalous matter, [core] and any cores of its type are no longer of a sufficient quality level to be compressed into a working core.")
- else
- say("[core] requires a minimum of a theoretical radius of [required] to successfully implode into a charged anomaly core.")
-
-/**
- * Determines how much explosive power (last value, so light impact theoretical radius) is required to make a certain anomaly type.
- *
- * Returns null if the max amount has already been reached.
- *
- * Arguments:
- * * anomaly_type - anomaly type define
- */
-/obj/machinery/research/explosive_compressor/proc/get_required_radius(anomaly_type)
- var/already_made = SSresearch.created_anomaly_types[anomaly_type]
- var/hard_limit = SSresearch.anomaly_hard_limit_by_type[anomaly_type]
- if(already_made >= hard_limit)
- return //return null
- // my crappy autoscale formula
- // linear scaling.
- var/radius_span = MAX_RADIUS_REQUIRED - MIN_RADIUS_REQUIRED
- var/radius_increase_per_core = radius_span / hard_limit
- var/radius = clamp(round(MIN_RADIUS_REQUIRED + radius_increase_per_core * already_made, 1), MIN_RADIUS_REQUIRED, MAX_RADIUS_REQUIRED)
- return radius
-
-/obj/machinery/research/explosive_compressor/attackby(obj/item/tool, mob/living/user, params)
- . = ..()
- if(istype(tool, /obj/item/raw_anomaly_core))
- if(inserted_core)
- to_chat(user, span_warning("There is already a core in [src]."))
- return
- if(testing)
- to_chat(user, span_warning("You can't insert [tool] into [src] while [p_theyre()] in testing mode."))
- return
- if(!user.transferItemToLoc(tool, src))
- to_chat(user, span_warning("[tool] is stuck to your hand."))
- return
- inserted_core = tool
- to_chat(user, span_notice("You insert [tool] into [src]."))
- return
- if(istype(tool, /obj/item/transfer_valve))
- // If they don't have a bomb core inserted, don't let them insert this. If they do, insert and do implosion.
- if(!inserted_core)
- to_chat(user, span_warning("There is no core inserted in [src]. What would be the point of detonating an implosion without a core?"))
- return
- if(testing)
- to_chat(user, span_warning("You can't insert [tool] into [src] while [p_theyre()] in testing mode."))
- return
- var/obj/item/transfer_valve/valve = tool
- if(!valve.ready())
- to_chat(user, span_warning("[valve] is incomplete."))
- return
- if(!user.transferItemToLoc(tool, src))
- to_chat(user, span_warning("[tool] is stuck to your hand."))
- return
- inserted_bomb = tool
- to_chat(user, span_notice("You insert [tool] and press the start button."))
- start_test()
-
-
-/**
- * Starts a compression test.
- */
-/obj/machinery/research/explosive_compressor/proc/start_test()
- if(!istype(inserted_core) || !istype(inserted_bomb))
- end_test("ERROR: Missing equpment. Items ejected.")
- return
-
- say("Beginning compression test. Opening transfer valve.")
- testing = TRUE
- test_status = null
- inserted_bomb.toggle_valve()
- timeout_timer = addtimer(CALLBACK(src, .proc/timeout_test), COMPRESSION_TEST_TIME, TIMER_STOPPABLE | TIMER_UNIQUE | TIMER_NO_HASH_WAIT)
- return
-
-/**
- * Ends a compression test.
- *
- * Arguments:
- * - message: A message for the compressor to say when the test ends.
- */
-/obj/machinery/research/explosive_compressor/proc/end_test(message)
- if(inserted_core)
- inserted_core.forceMove(drop_location())
- inserted_core = null
- if(inserted_bomb)
- inserted_bomb.forceMove(drop_location())
- inserted_bomb = null
- if(timeout_timer)
- QDEL_NULL(timeout_timer)
- if(message)
- say(message)
- testing = FALSE
- return
-
-/**
- * Checks whether an internal explosion was sufficient to compress the core.
- */
-/obj/machinery/research/explosive_compressor/proc/check_test(atom/source, list/arguments)
- SIGNAL_HANDLER
- . = COMSIG_CANCEL_EXPLOSION
- if(!inserted_core)
- test_status = "ERROR: No core present during detonation."
- return
-
- var/heavy = arguments[EXARG_KEY_DEV_RANGE]
- var/medium = arguments[EXARG_KEY_HEAVY_RANGE]
- var/light = arguments[EXARG_KEY_LIGHT_RANGE]
- var/explosion_range = max(heavy, medium, light, 0)
- var/required_range = get_required_radius(inserted_core.anomaly_type)
- var/turf/location = get_turf(src)
-
- var/cap_multiplier = SSmapping.level_trait(location.z, ZTRAIT_BOMBCAP_MULTIPLIER)
- if(isnull(cap_multiplier))
- cap_multiplier = 1
- var/capped_heavy = min(GLOB.MAX_EX_DEVESTATION_RANGE * cap_multiplier, heavy)
- var/capped_medium = min(GLOB.MAX_EX_HEAVY_RANGE * cap_multiplier, medium)
- SSexplosions.shake_the_room(location, explosion_range, (capped_heavy * 15) + (capped_medium * 20), capped_heavy, capped_medium)
-
- if(explosion_range < required_range)
- test_status = "Resultant detonation failed to produce enough implosive power to compress [inserted_core]. Core ejected."
- return
-
- if(test_status)
- return
- inserted_core = inserted_core.create_core(src, TRUE, TRUE)
- test_status = "Success. Resultant detonation has theoretical range of [explosion_range]. Required radius was [required_range]. Core production complete."
- return
-
-/**
- * Handles timing out the test after a while.
- */
-/obj/machinery/research/explosive_compressor/proc/timeout_test()
- timeout_timer = null
- if(!test_status)
- test_status = "Transfer valve resulted in negligible explosive power. Items ejected."
- end_test(test_status)
-
-#undef MAX_RADIUS_REQUIRED
-#undef MIN_RADIUS_REQUIRED
diff --git a/code/modules/research/destructive_analyzer.dm b/code/modules/research/destructive_analyzer.dm
index 65af7ec4355..47d5ada291e 100644
--- a/code/modules/research/destructive_analyzer.dm
+++ b/code/modules/research/destructive_analyzer.dm
@@ -97,7 +97,7 @@ Note: Must be placed within 3 tiles of the R&D Console
return FALSE
SSblackbox.record_feedback("nested tally", "item_deconstructed", 1, list("[TN.id]", "[loaded_item.type]"))
if(destroy_item(loaded_item))
- stored_research.boost_with_path(SSresearch.techweb_node_by_id(TN.id), dpath)
+ stored_research.boost_with_item(SSresearch.techweb_node_by_id(TN.id), dpath)
else
var/list/point_value = techweb_item_point_check(loaded_item)
diff --git a/code/modules/research/experimentor.dm b/code/modules/research/experimentor.dm
index a225699e40a..18b72f43189 100644
--- a/code/modules/research/experimentor.dm
+++ b/code/modules/research/experimentor.dm
@@ -197,7 +197,7 @@
if(dotype != FAIL)
var/list/nodes = techweb_item_boost_check(process)
var/picked = pick_weight(nodes) //This should work.
- stored_research.boost_with_path(SSresearch.techweb_node_by_id(picked), process.type)
+ stored_research.boost_with_item(SSresearch.techweb_node_by_id(picked), process.type)
updateUsrDialog()
/obj/machinery/rnd/experimentor/proc/matchReaction(matching,reaction)
diff --git a/code/modules/research/ordnance/_scipaper.dm b/code/modules/research/ordnance/_scipaper.dm
new file mode 100644
index 00000000000..5317dd15792
--- /dev/null
+++ b/code/modules/research/ordnance/_scipaper.dm
@@ -0,0 +1,355 @@
+#define SCIPAPER_COOPERATION_INDEX 1
+#define SCIPAPER_FUNDING_INDEX 2
+#define SCIENTIFIC_COOPERATION_PURCHASE_MULTIPLIER 0.01
+
+/// Scientific paper datum for retrieval and re-reading. A lot of the variables are there for fluff & flavor.
+/datum/scientific_paper
+ /// The title of our paper.
+ var/title
+ /// The principal author of our paper.
+ var/author
+ /// Whether this paper is co-authored or not.
+ var/et_alia = FALSE
+ /// Abstract.
+ var/abstract
+ /// The coop and funding gains from the paper.
+ var/list/gains
+ /// Experiment typepath.
+ var/datum/experiment/ordnance/experiment_path
+ /// The main "score" of a particular experiment.
+ var/tracked_variable
+ /**
+ * Derived from tracked_variable. Used for indexing and to reduce duplicates.
+ * Only one paper can be published in each tier for each experiment.
+ */
+ var/tier
+ /// The selected sponsor for our paper. Pathtype form.
+ var/datum/scientific_partner/partner_path
+
+/datum/scientific_paper/New()
+ . = ..()
+ set_amount()
+
+/**
+ * Calculate the gains of an experiment.
+ * Gain calculation follows a sigmoid curve.
+ * f(x) = L / (1+e^(-k(x-xo)))
+ * L is the upper limit. This should be the gain variable * 2.
+ * k is the steepness.
+ * x0 is the midpoint.
+ * x is our tracked variable.
+ * Returns the expected value of that tier.
+ */
+/datum/scientific_paper/proc/calculate_gains(calculated_tier)
+ if(!experiment_path || !tracked_variable)
+ return FALSE
+
+ var/datum/experiment/ordnance/initialized_experiment = locate(experiment_path) in SSresearch.ordnance_experiments
+ var/gain = initialized_experiment.gain[calculated_tier]
+ var/target_amount = initialized_experiment.target_amount[calculated_tier]
+ /// Steepness is calculated so that f(x=0) is always 1.
+ var/steepness = log(gain*2 - 1) / target_amount
+ var/calculated_gain = gain*2 / (1+NUM_E**(-steepness*(tracked_variable-target_amount)))
+ return calculated_gain
+
+/// Determine which tier can we publish at. Lower limit for an allowed tier is 10% of gain. Empty list if none are allowed.
+/datum/scientific_paper/proc/calculate_tier()
+ var/list/allowed_tiers = list()
+ if(!experiment_path || !tracked_variable)
+ return allowed_tiers
+ var/datum/experiment/ordnance/initialized_experiment = locate(experiment_path) in SSresearch.ordnance_experiments
+ for (var/each_tier in 1 to min(length(initialized_experiment.target_amount), length(initialized_experiment.gain)))
+ var/calculated_gain = calculate_gains(each_tier)
+ if(calculated_gain > 0.1 * initialized_experiment.gain[each_tier])
+ allowed_tiers += each_tier
+ return allowed_tiers
+
+/**
+ * Experiment -> Tier -> Gains
+ * Experiment -> Partners -> Gains
+ * Changing anything in the chain means those following it should be recounted.
+ */
+
+/**
+ * Used when assigning an experiment to a specific paper.
+ * Failing to provide a proper path, a tracked variable, or a correct data should null every non-fluff data.
+ * Implement this in the children procs.
+ */
+/datum/scientific_paper/proc/set_experiment(ex_path = null, variable = null, data = null)
+ return
+
+/// Sets a tier for us. Nulls the tier when called without args. Re-counts the amount.
+/datum/scientific_paper/proc/set_tier(assigned_tier = null)
+ tier = null
+ if(assigned_tier && (assigned_tier in calculate_tier()))
+ tier = assigned_tier
+ set_amount()
+
+/** Sets a specific partner for our paper. Partners give money and boost points.
+ * Partners exist separately from experiments and wont need to be reset every time something changes.
+ */
+/datum/scientific_paper/proc/set_partner(new_partner = null)
+ partner_path = null
+ if(ispath(new_partner, /datum/scientific_partner))
+ var/datum/scientific_partner/partner = locate(new_partner) in SSresearch.scientific_partners
+ if(experiment_path in partner.accepted_experiments)
+ partner_path = new_partner
+ set_amount()
+
+/** Does the counting for gains.
+ * Call this whenever experiments/tier/partners are changed.
+ * Handled automatically, calling this proc externally shouldn't be necessary.
+ * Also doubles as an initialization for the gains list.
+ */
+/datum/scientific_paper/proc/set_amount()
+ gains = list(SCIPAPER_COOPERATION_INDEX = 0, SCIPAPER_FUNDING_INDEX = 0)
+ if(!tier || !experiment_path || !tracked_variable)
+ return FALSE
+ var/gain = calculate_gains(tier)
+ for (var/gain_type in 1 to gains.len)
+ gains[gain_type] = gain
+ if(!partner_path)
+ continue
+ var/datum/scientific_partner/partner = locate(partner_path) in SSresearch.scientific_partners
+ gains[gain_type] *= partner.multipliers[gain_type]
+
+/** Fully check if our paper have all the required variables, and prevent duplicate papers being published in the same tier.
+ * Things to check: tier, gain, and partner here. ex_path and record datums in subtypes.
+ */
+/datum/scientific_paper/proc/allowed_to_publish(datum/techweb/techweb_to_check)
+ if(!tier || !gains || !partner_path || (0 in gains))
+ return FALSE
+ return techweb_to_check.published_papers[experiment_path][tier] ? FALSE : TRUE
+
+/datum/scientific_paper/proc/publish_paper(datum/techweb/techweb_to_publish)
+ autofill()
+ techweb_to_publish.published_papers[experiment_path][tier] = src
+ techweb_to_publish.scientific_cooperation[partner_path] += gains[SCIPAPER_COOPERATION_INDEX]
+ if(istype(techweb_to_publish, /datum/techweb/science))
+ var/datum/bank_account/dept_budget = SSeconomy.get_dep_account(ACCOUNT_SCI)
+ if(dept_budget)
+ dept_budget.adjust_money(gains[SCIPAPER_FUNDING_INDEX] * 1000)
+
+/**
+ * Clones into a new paper type.
+ * Important (non-fluff) variables will be carried over and should be cleaned with set_experiment by whoever is calling this.
+ *
+ * clone_into your own typepath will be like a normal clone.
+ *
+ * If you want to subtype this, do it in a way that doesn't mess with the type change.
+ */
+/datum/scientific_paper/proc/clone_into(typepath)
+ var/datum/scientific_paper/new_paper = new typepath
+ new_paper.title = title
+ new_paper.author = author
+ new_paper.et_alia = et_alia
+ new_paper.abstract = abstract
+ new_paper.gains = gains
+ new_paper.experiment_path = experiment_path
+ new_paper.tracked_variable = tracked_variable
+ new_paper.tier = tier
+ new_paper.partner_path = partner_path
+ return new_paper
+
+/// Returns the formatted, readable gist of our paper in a list.
+/datum/scientific_paper/proc/return_gist()
+ var/list/gist = list()
+ var/list/transcripted_gains = list(SCIPAPER_COOPERATION_INDEX, SCIPAPER_FUNDING_INDEX)
+ for (var/index in 1 to transcripted_gains.len)
+ if (!gains)
+ transcripted_gains[index] = "None"
+ continue
+ switch (round(gains[index]))
+ if(-INFINITY to 0)
+ transcripted_gains[index] = "None"
+ if(1 to 24)
+ transcripted_gains[index] = "Little"
+ if(25 to 49)
+ transcripted_gains[index] = "Moderate"
+ if(50 to 99)
+ transcripted_gains[index] = "Significant"
+ if(100 to INFINITY)
+ transcripted_gains[index] = "Huge"
+ else
+ transcripted_gains[index] = "Undefined"
+ gist["gains"] = transcripted_gains
+ gist["title"] = title
+ gist["author"] = author
+ gist["etAlia"] = et_alia
+ gist["abstract"] = abstract
+ gist["experimentName"] = initial(experiment_path?.name)
+ gist["tier"] = tier
+ gist["partner"] = initial(partner_path?.name)
+ return gist
+
+/datum/scientific_paper/proc/autofill()
+ if(!title)
+ title = "On [initial(experiment_path.name)] - [tier]"
+ if(!author)
+ author = "Unknown"
+ et_alia = FALSE
+ if(!abstract)
+ abstract = "Published on [station_time_timestamp()]"
+
+/datum/scientific_paper/explosive
+ /**
+ * Used to check explosive experiments that needs to be unique.
+ * References a tachyon record where applicable.
+ */
+ var/datum/data/tachyon_record/explosion_record
+
+/// Check if our explosion has already been published and whether the experiment path is correct or not.
+/datum/scientific_paper/explosive/allowed_to_publish(datum/techweb/techweb_to_check)
+ if(!ispath(experiment_path, /datum/experiment/ordnance/explosive))
+ return FALSE
+ if(!istype(explosion_record))
+ return FALSE
+ var/list/published_papers = techweb_to_check.published_papers
+ for(var/experiment_path in published_papers)
+ for(var/datum/scientific_paper/explosive/papers in published_papers[experiment_path])
+ var/datum/data/tachyon_record/record_to_check = papers.explosion_record
+ if(explosion_record.explosion_identifier == record_to_check.explosion_identifier)
+ return FALSE
+ . = ..()
+
+/datum/scientific_paper/explosive/set_experiment(ex_path = null, variable = null, data = null)
+ var/invalid = FALSE
+
+ invalid = invalid || !ispath(ex_path, /datum/experiment/ordnance/explosive)
+ invalid = invalid || !variable
+ invalid = invalid || !istype(data, /datum/data/tachyon_record)
+
+ if(invalid)
+ experiment_path = null
+ tracked_variable = null
+ explosion_record = null
+ else
+ experiment_path = ex_path
+ tracked_variable = variable
+ explosion_record = data
+
+ set_tier()
+ set_partner()
+
+/datum/scientific_paper/explosive/clone_into(typepath)
+ if(typepath != type)
+ return ..()
+
+ var/datum/scientific_paper/explosive/new_paper = ..(typepath)
+ new_paper.explosion_record = explosion_record
+ return new_paper
+
+/datum/scientific_paper/gaseous
+ var/datum/data/compressor_record/compressor_record
+
+/**
+ * Check if our record datum is a duplicate or no.
+ * No index number is necessary because compressor records cant be replicated.
+ * Also checks the experiment path.
+ */
+/datum/scientific_paper/gaseous/allowed_to_publish(datum/techweb/techweb_to_check)
+ if(!ispath(experiment_path, /datum/experiment/ordnance/gaseous))
+ return FALSE
+ if(!istype(compressor_record))
+ return FALSE
+ var/list/published_papers = techweb_to_check.published_papers
+ for(var/experiment_path in published_papers)
+ for(var/datum/scientific_paper/gaseous/papers in published_papers[experiment_path])
+ if(compressor_record == papers.compressor_record)
+ return FALSE
+ . = ..()
+
+/datum/scientific_paper/gaseous/set_experiment(ex_path = null, variable = null, data = null)
+ var/invalid = FALSE
+
+ invalid = invalid || !ispath(ex_path, /datum/experiment/ordnance/gaseous)
+ invalid = invalid || !variable
+ invalid = invalid || !istype(data, /datum/data/compressor_record)
+
+ if(invalid)
+ experiment_path = null
+ tracked_variable = null
+ compressor_record = null
+ else
+ experiment_path = ex_path
+ tracked_variable = variable
+ compressor_record = data
+
+ set_tier()
+ set_partner()
+
+/datum/scientific_paper/gaseous/clone_into(typepath)
+ if(typepath != type)
+ return ..()
+
+ var/datum/scientific_paper/gaseous/new_paper = ..(typepath)
+ new_paper.compressor_record = compressor_record
+ return new_paper
+
+/// Various informations on companies/scientific programs/journals etc that the players can sign on to.
+/datum/scientific_partner
+ var/name
+ /// Brief explanation of the associated program. Can be used for lore.
+ var/flufftext
+ /// Cash and renown multiplier for allying with this partner.
+ var/list/multipliers = list(SCIPAPER_COOPERATION_INDEX = 1, SCIPAPER_FUNDING_INDEX = 1)
+ /// List of ordnance experiments that our partner is willing to accept. If this list is not filled it means the partner will accept everything.
+ var/list/accepted_experiments = list()
+ /// Associative list of which technology the partner might be able to boost and by how much.
+ var/list/boosted_nodes = list()
+
+
+/datum/scientific_partner/proc/purchase_boost(datum/techweb/purchasing_techweb, datum/techweb_node/node)
+ if(!allowed_to_boost(purchasing_techweb, node.id))
+ return FALSE
+ purchasing_techweb.boost_techweb_node(node, list(TECHWEB_POINT_TYPE_GENERIC=boosted_nodes[node.id]))
+ purchasing_techweb.scientific_cooperation[type] -= boosted_nodes[node.id] * SCIENTIFIC_COOPERATION_PURCHASE_MULTIPLIER
+ return TRUE
+
+/datum/scientific_partner/proc/allowed_to_boost(datum/techweb/purchasing_techweb, node_id)
+ if(purchasing_techweb.scientific_cooperation[type] < (boosted_nodes[node_id] * SCIENTIFIC_COOPERATION_PURCHASE_MULTIPLIER)) // Too expensive
+ return FALSE
+ if(!(node_id in purchasing_techweb.get_available_nodes())) // Not currently available
+ return FALSE
+ if((TECHWEB_POINT_TYPE_GENERIC in purchasing_techweb.boosted_nodes[node_id]) && (purchasing_techweb.boosted_nodes[node_id][TECHWEB_POINT_TYPE_GENERIC] >= boosted_nodes[node_id])) // Already bought or we have a bigger discount
+ return FALSE
+ return TRUE
+
+/datum/computer_file/data/ordnance
+ /// List of experiments filtered by doppler array or populated by the tank compressor. Experiment path as key, score as value.
+ var/list/possible_experiments
+ size = 4
+ filetype = "ORD"
+
+/datum/computer_file/data/ordnance/proc/return_data()
+ return null
+
+/datum/computer_file/data/ordnance/clone()
+ var/datum/computer_file/data/ordnance/temp = ..()
+ temp.possible_experiments = possible_experiments
+ return temp
+
+/datum/computer_file/data/ordnance/explosive
+ /// Tachyon record, used for an explosive experiment.
+ var/datum/data/tachyon_record/explosion_record
+ filetype = "DOP"
+
+/datum/computer_file/data/ordnance/explosive/return_data()
+ return explosion_record
+
+/datum/computer_file/data/ordnance/explosive/clone()
+ var/datum/computer_file/data/ordnance/explosive/temp = ..()
+ temp.explosion_record = explosion_record
+ return temp
+
+/datum/computer_file/data/ordnance/gaseous
+ var/datum/data/compressor_record/gas_record
+ filetype = "COM"
+
+/datum/computer_file/data/ordnance/gaseous/return_data()
+ return gas_record
+
+/datum/computer_file/data/ordnance/gaseous/clone()
+ var/datum/computer_file/data/ordnance/gaseous/temp = ..()
+ temp.gas_record = gas_record
+ return temp
diff --git a/code/modules/research/ordnance/doppler_array.dm b/code/modules/research/ordnance/doppler_array.dm
new file mode 100644
index 00000000000..d3f0eb30006
--- /dev/null
+++ b/code/modules/research/ordnance/doppler_array.dm
@@ -0,0 +1,315 @@
+/obj/machinery/doppler_array
+ name = "tachyon-doppler array"
+ desc = "A highly precise directional sensor array which measures the release of quants from decaying tachyons. The doppler shifting of the mirror-image formed by these quants can reveal the size, location and temporal affects of energetic disturbances within a large radius ahead of the array.\n"
+ circuit = /obj/item/circuitboard/machine/doppler_array
+ icon = 'icons/obj/machines/research.dmi'
+ icon_state = "tdoppler"
+ base_icon_state = "tdoppler"
+ density = TRUE
+ verb_say = "states coldly"
+ var/cooldown = 10
+ var/next_announce = 0
+ var/max_dist = 150
+ /// Number which will be part of the name of the next record, increased by one for each already created record
+ var/record_number = 1
+ /// List of all explosion records in the form of /datum/data/tachyon_record
+ var/list/records = list()
+ /// Reference to a disk we are going to print to.
+ var/obj/item/computer_hardware/hard_drive/portable/inserted_disk
+
+ // Lighting system to better communicate the directions.
+ light_system = MOVABLE_LIGHT_DIRECTIONAL
+ light_range = 4
+ light_power = 1
+ light_color = COLOR_RED
+
+/obj/machinery/doppler_array/Initialize(mapload)
+ . = ..()
+ RegisterSignal(SSdcs, COMSIG_GLOB_EXPLOSION, .proc/sense_explosion)
+ RegisterSignal(src, COMSIG_MACHINERY_POWER_LOST, .proc/update_doppler_light)
+ RegisterSignal(src, COMSIG_MACHINERY_POWER_RESTORED, .proc/update_doppler_light)
+ update_doppler_light()
+
+ // Rotation determines the detectable direction.
+ AddComponent(/datum/component/simple_rotation)
+
+/datum/data/tachyon_record
+ name = "Log Recording"
+ var/timestamp
+ var/coordinates = ""
+ var/displacement = 0
+ var/factual_radius = list()
+ var/theory_radius = list()
+ /// Indexed to length 3 if filled properly. Should be an empty list otherwise.
+ var/reaction_results = list()
+ var/explosion_identifier
+
+/obj/machinery/doppler_array/examine(mob/user)
+ . = ..()
+ . += span_notice("It is currently facing [dir2text(dir)]")
+
+/obj/machinery/doppler_array/attackby(obj/item/item, mob/user, params)
+ if(istype(item, /obj/item/computer_hardware/hard_drive/portable))
+ var/obj/item/computer_hardware/hard_drive/portable/disk = item
+ eject_disk(user)
+ if(user.transferItemToLoc(disk, src))
+ inserted_disk = disk
+ return
+ else
+ balloon_alert(user, span_warning("[disk] is stuck to your hand."))
+ return ..()
+ return ..()
+
+/obj/machinery/doppler_array/wrench_act(mob/living/user, obj/item/tool)
+ if(!default_unfasten_wrench(user, tool))
+ return FALSE
+ return TRUE
+
+/obj/machinery/doppler_array/screwdriver_act(mob/living/user, obj/item/tool)
+ if(!default_deconstruction_screwdriver(user, "[base_icon_state]", "[base_icon_state]", tool))
+ return FALSE
+ power_change()
+ update_appearance()
+ return TRUE
+
+/obj/machinery/doppler_array/crowbar_act(mob/living/user, obj/item/tool)
+ if(!default_deconstruction_crowbar(tool))
+ return FALSE
+ return TRUE
+
+/// Printing of a record into a disk.
+/obj/machinery/doppler_array/proc/print(mob/user, datum/data/tachyon_record/record)
+ if(!record || !inserted_disk)
+ return
+
+ var/datum/computer_file/data/ordnance/explosive/record_data = new
+ record_data.filename = "Doppler Array " + record.name //Doppler Array Log Recording #x
+ record_data.explosion_record = record
+ record_data.possible_experiments = apply_experiments(record)
+
+ if(inserted_disk.store_file(record_data))
+ playsound(src, 'sound/machines/ping.ogg', 25)
+ else
+ playsound(src, 'sound/machines/terminal_error.ogg', 25)
+
+/**
+ * Checks a specified tachyon record for fitting reactions, then returns a list with
+ * the experiment typepath as key and score as value.
+ * The score is the same for all explosive experiments (light radius).
+ */
+/obj/machinery/doppler_array/proc/apply_experiments(datum/data/tachyon_record/record)
+ var/list/passed_experiments = list()
+ var/list/record_reactions = record.reaction_results
+
+ var/list/radius_used = length(record.theory_radius) ? record.theory_radius : record.factual_radius
+ var/explosion_score = radius_used["shockwave_radius"]
+ if(!explosion_score) // Dont even bother.
+ return passed_experiments
+
+ /// Early return for explosions without proper reaction_results. We will append experiments that are always fair game.
+ if(!record_reactions.len)
+ for (var/datum/experiment/ordnance/explosive/experiment in SSresearch.ordnance_experiments)
+ if(experiment.allow_any_source)
+ passed_experiments += list(experiment.type = explosion_score)
+ return passed_experiments
+
+ for (var/datum/experiment/ordnance/explosive/experiment in SSresearch.ordnance_experiments)
+ var/list/experiment_reactions = experiment.required_reactions
+ /// The reactions in record.reaction_results that corresponds to the required_reactions list.
+ var/list/fitting_reactions = list()
+ /// The reactions in required_reactions list that are missing in record.reaction_results.
+ var/list/missing_reactions = list()
+ /// The reactions in record.reaction_results that are not present in required_reactions list.
+ var/list/extra_reactions = list()
+
+ for(var/reaction in experiment_reactions)
+ if(reaction in record_reactions[TANK_RESULTS_REACTION])
+ fitting_reactions += reaction
+ else
+ missing_reactions += reaction
+
+ for(var/reaction in record_reactions[TANK_RESULTS_REACTION])
+ if(!(reaction in fitting_reactions))
+ extra_reactions += reaction
+
+ // Check for extra reactions.
+ if(experiment.sanitized_reactions && extra_reactions.len)
+ continue
+ // Check for misc properties
+ if(experiment.sanitized_misc && length(record_reactions[TANK_RESULTS_MISC]))
+ continue
+ // Check for missing reactions for those who require all.
+ if(experiment.require_all && missing_reactions.len)
+ continue
+ // For those who only need one, check if there is actually one.
+ if(!experiment.require_all && experiment_reactions.len)
+ if(!fitting_reactions.len)
+ continue
+
+ // Suceeded all check, append to the returned list.
+ passed_experiments += list(experiment.type = explosion_score)
+
+ return passed_experiments
+
+/// Sensing, recording, and broadcasting of explosion
+/obj/machinery/doppler_array/proc/sense_explosion(datum/source, turf/epicenter, devastation_range, heavy_impact_range, light_impact_range,
+ took, orig_dev_range, orig_heavy_range, orig_light_range, explosion_cause, explosion_index)
+ SIGNAL_HANDLER
+ var/list/fetched_reaction_results = list()
+
+ if(istype(explosion_cause, /obj/item/tank))
+ var/obj/item/tank/exploding_tank = explosion_cause
+ fetched_reaction_results = exploding_tank.explosion_information()
+
+ if(machine_stat & NOPOWER)
+ return FALSE
+ var/turf/zone = get_turf(src)
+ if(zone.z != epicenter.z)
+ return FALSE
+
+ if(next_announce > world.time)
+ return FALSE
+ next_announce = world.time + cooldown
+
+ if((get_dist(epicenter, zone) > max_dist) || !(get_dir(zone, epicenter) & dir))
+ return FALSE
+
+ var/datum/data/tachyon_record/new_record = new /datum/data/tachyon_record()
+ new_record.name = "Log Recording #[record_number]"
+ new_record.timestamp = station_time_timestamp()
+ new_record.coordinates = "[epicenter.x], [epicenter.y]"
+ new_record.displacement = took
+ new_record.factual_radius["epicenter_radius"] = devastation_range
+ new_record.factual_radius["outer_radius"] = heavy_impact_range
+ new_record.factual_radius["shockwave_radius"] = light_impact_range
+ new_record.reaction_results = fetched_reaction_results
+ new_record.explosion_identifier = explosion_index
+
+ var/list/messages = list("Explosive disturbance detected.",
+ "Epicenter at: grid ([epicenter.x], [epicenter.y]). Temporal displacement of tachyons: [took] seconds.",
+ "Factual: Epicenter radius: [devastation_range]. Outer radius: [heavy_impact_range]. Shockwave radius: [light_impact_range].",
+ )
+
+ // If the bomb was capped, say its theoretical size.
+ if(devastation_range < orig_dev_range || heavy_impact_range < orig_heavy_range || light_impact_range < orig_light_range)
+ messages += "Theoretical: Epicenter radius: [orig_dev_range]. Outer radius: [orig_heavy_range]. Shockwave radius: [orig_light_range]."
+ new_record.theory_radius["epicenter_radius"] = orig_dev_range
+ new_record.theory_radius["outer_radius"] = orig_heavy_range
+ new_record.theory_radius["shockwave_radius"] = orig_light_range
+
+ for(var/message in messages)
+ say(message)
+
+ record_number++
+ records += new_record
+
+ return TRUE
+
+/obj/machinery/doppler_array/proc/eject_disk(mob/user)
+ if(!inserted_disk)
+ return FALSE
+ if(user)
+ user.put_in_hands(inserted_disk)
+ else
+ inserted_disk.forceMove(drop_location())
+ playsound(src, 'sound/machines/card_slide.ogg', 50)
+ return TRUE
+
+/// We rely on exited to clear references.
+/obj/machinery/doppler_array/Exited(atom/movable/gone, direction)
+ if(gone == inserted_disk)
+ inserted_disk = null
+ . = ..()
+
+/obj/machinery/doppler_array/powered()
+ if(panel_open)
+ return FALSE
+ return ..()
+
+/obj/machinery/doppler_array/update_overlays()
+ . = ..()
+ if(panel_open)
+ . += mutable_appearance(icon, "[base_icon_state]_open")
+ else
+ . += mutable_appearance(icon,"[base_icon_state]_cable")
+
+ if(machine_stat & BROKEN) // Probably meant to be used on an indestructible doppler, but this is not implemented.
+ . += mutable_appearance(icon, "[base_icon_state]_screen-broken")
+ else if (machine_stat & NOPOWER)
+ . += mutable_appearance(icon, "[base_icon_state]_screen-off")
+
+/obj/machinery/doppler_array/on_deconstruction()
+ eject_disk()
+ . = ..()
+
+/obj/machinery/doppler_array/Destroy()
+ inserted_disk = null
+ QDEL_NULL(records) //We only want the list nuked, not the contents.
+ . = ..()
+
+/obj/machinery/doppler_array/proc/update_doppler_light()
+ SIGNAL_HANDLER
+ set_light_on(!(machine_stat & NOPOWER))
+
+/obj/machinery/doppler_array/AltClick(mob/user)
+ return ..() // This hotkey is BLACKLISTED since it's used by /datum/component/simple_rotation
+
+/obj/machinery/doppler_array/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "DopplerArray", name)
+ ui.open()
+
+/obj/machinery/doppler_array/ui_data(mob/user)
+ var/list/data = list()
+ data["records"] = list()
+ data["disk"] = inserted_disk?.name
+ data["storage"] = "[inserted_disk?.used_capacity] / [inserted_disk?.max_capacity] GQ"
+ for(var/datum/data/tachyon_record/singular_record in records)
+ var/list/record_data = list(
+ "name" = singular_record.name,
+ "timestamp" = singular_record.timestamp,
+ "coordinates" = singular_record.coordinates,
+ "displacement" = singular_record.displacement,
+ "factual_epicenter_radius" = singular_record.factual_radius["epicenter_radius"],
+ "factual_outer_radius" = singular_record.factual_radius["outer_radius"],
+ "factual_shockwave_radius" = singular_record.factual_radius["shockwave_radius"],
+ "theory_epicenter_radius" = singular_record.theory_radius["epicenter_radius"],
+ "theory_outer_radius" = singular_record.theory_radius["outer_radius"],
+ "theory_shockwave_radius" = singular_record.theory_radius["shockwave_radius"],
+ "reaction_results" = list(),
+ "ref" = REF(singular_record)
+ )
+
+ var/list/reaction_data = singular_record.reaction_results
+ // Make sure the list is indexed first.
+ if(reaction_data.len)
+ for (var/path in reaction_data[TANK_RESULTS_REACTION])
+ var/datum/gas_reaction/reaction_path = path
+ record_data["reaction_results"] += initial(reaction_path.name)
+ if(TANK_MERGE_OVERPRESSURE in reaction_data[TANK_RESULTS_MISC])
+ record_data["reaction_results"] += "Tank overpressurized before reaction"
+
+ data["records"] += list(record_data)
+ return data
+
+/obj/machinery/doppler_array/ui_act(action, list/params)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("delete_record")
+ var/datum/data/tachyon_record/record = locate(params["ref"]) in records
+ if(!records || !(record in records))
+ return
+ records -= record
+ return TRUE
+ if("print_record")
+ var/datum/data/tachyon_record/record = locate(params["ref"]) in records
+ if(!records || !(record in records))
+ return
+ print(usr, record)
+ return TRUE
+ if("eject_disk")
+ eject_disk(usr)
diff --git a/code/modules/research/ordnance/scipaper_partner.dm b/code/modules/research/ordnance/scipaper_partner.dm
new file mode 100644
index 00000000000..897172decde
--- /dev/null
+++ b/code/modules/research/ordnance/scipaper_partner.dm
@@ -0,0 +1,47 @@
+/datum/scientific_partner/spinward_science
+ name="Spinward Science"
+ flufftext="A local scientific community started by the diverse inhabitants of the Spinward Sector. Not generally advanced, but they will gladly work with us."
+ multipliers=list(SCIPAPER_COOPERATION_INDEX = 1, SCIPAPER_FUNDING_INDEX=0.75)
+ boosted_nodes=list("emp_basic" = 500, "NVGtech" = 1500, "integrated_HUDs" = 500)
+
+/datum/scientific_partner/baron
+ name = "Ghost Writing"
+ flufftext="A nearby research station ran by a very wealthy captain seems to be struggling with their scientific output. They might reward us handsomely if we ghostwrite for them."
+ multipliers = list(SCIPAPER_COOPERATION_INDEX = 0.25, SCIPAPER_FUNDING_INDEX=5)
+ boosted_nodes = list("comp_recordkeeping" = 500, "computer_hardware_basic"=500)
+
+/datum/scientific_partner/defense
+ name="Defense Partnership"
+ flufftext="We can work directly for Nanotrasen's \[REDACTED\] division, potentially providing us access with advanced defensive gadgets."
+ accepted_experiments=list(/datum/experiment/ordnance/explosive/lowyieldbomb, /datum/experiment/ordnance/explosive/highyieldbomb, /datum/experiment/ordnance/explosive/pressurebomb)
+ boosted_nodes = list("adv_weaponry" = 5000, "weaponry" = 2500, "sec_basic" = 1250, "explosive_weapons"=1250)
+
+/datum/scientific_partner/energy
+ name="High-Energy Research"
+ flufftext="A recently established high-energy research concern started by Nanotrasen. They might be able to assist our energy-based research."
+ accepted_experiments=list(/datum/experiment/ordnance/explosive/hydrogenbomb, /datum/experiment/ordnance/explosive/nobliumbomb)
+ boosted_nodes = list("adv_beam_weapons" = 1250, "beam_weapons" = 1250, "electronic_weapons"=1250, "mech_laser"=1250, "mech_laser_heavy"=1250)
+
+/datum/scientific_partner/engineering
+ name="Corps of Engineers"
+ flufftext = "Many engineers are interested in the application of exotic gases in their day-to-day work. They might be able to offer us information on some their gadgets in return."
+ accepted_experiments=list(/datum/experiment/ordnance/gaseous/halon, /datum/experiment/ordnance/gaseous/noblium, /datum/experiment/ordnance/explosive/lowyieldbomb)
+ boosted_nodes=list(/datum/techweb_node/adv_engi=2500, /datum/techweb_node/adv_power=1500, /datum/techweb_node/bluespace_power=2000, /datum/techweb_node/high_efficiency=2500, /datum/techweb_node/micro_bluespace=2500)
+
+/datum/scientific_partner/medical
+ name="Biological Research Division"
+ flufftext="A collegiate of the best medical researchers Nanotrason employs. They seem to be interested in the biological effects of some more exotic gases."
+ accepted_experiments=list(/datum/experiment/ordnance/gaseous/nitrium, /datum/experiment/ordnance/gaseous/bz)
+ boosted_nodes=list("cyber_organs"=750, "cyber_organs_upgraded"=1000, "genetics"=500, "subdermal_implants"=1250, "adv_biotech"=1000)
+
+/datum/scientific_partner/ordnance
+ name="Ordnance Partners"
+ flufftext="There are other stations tasked with researching the more esoteric reactions. We might be able to exchange some information with them."
+ accepted_experiments=list(/datum/experiment/ordnance/explosive/pressurebomb, /datum/experiment/ordnance/explosive/nobliumbomb)
+ boosted_nodes=list("gravity_gun"=1250, "mecha_phazon"=1500, "mech_wormhole_gen"=1250, "bluespace_travel"=1000, "micro_bluespace"=3000, "basic_plasma"=1000, "adv_plasma"=1000)
+
+/datum/scientific_partner/cold_physics
+ name="Low Temperature Research"
+ flufftext="A Nanotrasen division researching matter interactions at very low temperatures. Very interested in our hyper-noblium research."
+ accepted_experiments=list(/datum/experiment/ordnance/gaseous/noblium, /datum/experiment/ordnance/explosive/nobliumbomb)
+ boosted_nodes=list("emp_super" = 3000, "emp_adv"=1250, "cryotech"=1500)
diff --git a/code/modules/research/ordnance/scipaper_publisher.dm b/code/modules/research/ordnance/scipaper_publisher.dm
new file mode 100644
index 00000000000..22dffae8277
--- /dev/null
+++ b/code/modules/research/ordnance/scipaper_publisher.dm
@@ -0,0 +1,227 @@
+/datum/computer_file/program/scipaper_program
+ filename = "ntfrontier"
+ filedesc = "NT Frontier"
+ category = PROGRAM_CATEGORY_SCI
+ extended_desc = "Scientific paper publication and navigation software."
+ requires_ntnet = TRUE
+ size = 12
+ program_icon_state = "research"
+ tgui_id = "NtosScipaper"
+ program_icon = "paper-plane"
+ transfer_access = ACCESS_ORDNANCE
+
+ var/datum/techweb/linked_techweb
+ /// Unpublished, temporary paper datum.
+ var/datum/scientific_paper/paper_to_be
+ /// Here to reduce load. Corresponds to the tab in the UI.
+ var/current_tab = 1
+ /// The file under consideration.
+ var/datum/computer_file/data/ordnance/selected_file
+
+/datum/computer_file/program/scipaper_program/New()
+ . = ..()
+ paper_to_be = new
+
+/datum/computer_file/program/scipaper_program/run_program(mob/living/user)
+ . = ..()
+ linked_techweb = SSresearch.science_tech
+
+/datum/computer_file/program/scipaper_program/proc/recheck_file_presence()
+ if(selected_file in holder.stored_files)
+ return FALSE
+ UnregisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED)
+ selected_file = null
+ paper_to_be.set_experiment()
+
+/datum/computer_file/program/scipaper_program/ui_static_data(mob/user)
+ var/list/data = list()
+ var/list/parsed_experiments = list()
+ for (var/datum/experiment/ordnance/experiment in SSresearch.ordnance_experiments)
+ var/list/singular_experiment = list()
+ singular_experiment["path"] = experiment.type
+ singular_experiment["name"] = experiment.name
+ singular_experiment["description"] = experiment.description
+ singular_experiment["target"] = experiment.target_amount
+ if(istype(experiment, /datum/experiment/ordnance/explosive))
+ singular_experiment["suffix"] = "tiles"
+ singular_experiment["prefix"] = "Range"
+ else if(istype(experiment, /datum/experiment/ordnance/gaseous))
+ singular_experiment["suffix"] = "moles"
+ singular_experiment["prefix"] = "Gas"
+ parsed_experiments += list(singular_experiment)
+
+ var/list/parsed_partners = list()
+ for (var/datum/scientific_partner/partner in SSresearch.scientific_partners)
+ var/list/singular_partner = list()
+ singular_partner["name"] = partner.name
+ singular_partner["flufftext"] = partner.flufftext
+ singular_partner["multipliers"] = partner.multipliers
+ singular_partner["path"] = partner.type
+ singular_partner["boostedNodes"] = list()
+ singular_partner["acceptedExperiments"] = list()
+ for (var/node_id in partner.boosted_nodes)
+ var/datum/techweb_node/node = SSresearch.techweb_node_by_id(node_id)
+ singular_partner["boostedNodes"] += list(list("name" = node.display_name, "discount" = partner.boosted_nodes[node_id], "id"=node_id))
+ for (var/datum/experiment/ordnance/ordnance_experiment as anything in partner.accepted_experiments)
+ singular_partner["acceptedExperiments"] += initial(ordnance_experiment.name)
+ parsed_partners += list(singular_partner)
+
+ data["experimentInformation"] = parsed_experiments
+ data["partnersInformation"] = parsed_partners
+ data["coopIndex"] = SCIPAPER_COOPERATION_INDEX
+ data["fundingIndex"] = SCIPAPER_FUNDING_INDEX
+ return data
+
+/datum/computer_file/program/scipaper_program/ui_data()
+ // Program Headers:
+ var/list/data = get_header_data()
+ data["currentTab"] = current_tab
+
+ // First page. Form submission.
+ if(current_tab == 1)
+ data["fileList"] = list()
+ data["expList"] = list()
+ data["allowedTiers"] = list()
+ data["allowedPartners"] = list()
+ // Both the file and experiment list are assoc lists. ID as value, display name as keys.
+ for(var/datum/computer_file/data/ordnance/ordnance_file in holder.stored_files)
+ data["fileList"] += list(ordnance_file.filename = ordnance_file.uid)
+ if(selected_file)
+ for (var/possible_experiment in selected_file.possible_experiments)
+ var/datum/experiment/ordnance/experiment = possible_experiment
+ data["expList"] += list(initial(experiment.name) = experiment)
+ data["allowedTiers"] = paper_to_be.calculate_tier()
+ for (var/partner in SSresearch.scientific_partners)
+ var/datum/scientific_partner/scientific_partner = partner
+ if(paper_to_be.experiment_path in scientific_partner.accepted_experiments)
+ data["allowedPartners"] += list(scientific_partner.name = scientific_partner.type)
+
+ data += paper_to_be.return_gist()
+ data["selectedFile"] = selected_file?.filename
+ // Renamed both of these to be more topical.
+ data["selectedExperiment"] = data["experimentName"]
+ data -= "experimentName"
+ data["selectedPartner"] = data["partner"]
+ data -= "partner"
+
+ // Second page. View previous
+ if(current_tab == 2)
+ data["publishedPapers"] = list()
+ for (var/experiment_types in linked_techweb.published_papers)
+ for (var/datum/scientific_paper/paper in linked_techweb.published_papers[experiment_types])
+ data["publishedPapers"] += list(paper.return_gist())
+
+ if(current_tab == 4)
+ data["purchaseableBoosts"] = list()
+ data["relations"] = list()
+ var/list/visible_nodes = list()
+ visible_nodes += linked_techweb.get_available_nodes()
+ visible_nodes += linked_techweb.get_researched_nodes()
+ data["visibleNodes"] = list()
+ for (var/id in visible_nodes)
+ if(visible_nodes[id])
+ data["visibleNodes"] += id
+
+ for (var/datum/scientific_partner/partner as anything in SSresearch.scientific_partners)
+ var/relations = linked_techweb.scientific_cooperation[partner.type]
+ switch (round(relations / SCIENTIFIC_COOPERATION_PURCHASE_MULTIPLIER)) // We use points to determine these
+ if(-INFINITY to 0)
+ data["relations"][partner.type] = "Nonexistant"
+ if(1 to 2499)
+ data["relations"][partner.type] = "Negligible"
+ if(2500 to 4999)
+ data["relations"][partner.type] = "Limited"
+ if(5000 to 9999)
+ data["relations"][partner.type] = "Cordial"
+ if(10000 to 19999)
+ data["relations"][partner.type] = "Partners"
+ if(20000 to INFINITY)
+ data["relations"][partner.type] = "Devoted"
+ else
+ data["relations"][partner.type] = "Undefined"
+ data["purchaseableBoosts"][partner.type] = list()
+ for(var/node_id in linked_techweb.get_available_nodes())
+ // Not from our partner
+ if(!(node_id in partner.boosted_nodes))
+ continue
+ if(!partner.allowed_to_boost(linked_techweb, node_id))
+ continue
+ data["purchaseableBoosts"][partner.type] += node_id
+ return data
+
+/datum/computer_file/program/scipaper_program/ui_act(action, params)
+ . = ..()
+ if (.)
+ return
+
+ switch(action)
+ if("et_alia")
+ paper_to_be.et_alia = !paper_to_be.et_alia
+ return TRUE
+ // Handle the publication
+ if("publish")
+ publish()
+ return TRUE
+ // For every change in the input, we correspond it with the paper_data list and update it.
+ if("rewrite")
+ if(length(params))
+ for (var/changed_entry in params)
+ if (changed_entry == "title")
+ paper_to_be.title = sanitize(params[changed_entry])
+ if (changed_entry == "author")
+ paper_to_be.author = sanitize(params[changed_entry])
+ if (changed_entry == "abstract")
+ paper_to_be.abstract = sanitize(params[changed_entry])
+ return TRUE
+ if("change_tab")
+ current_tab = params["new_tab"]
+ return TRUE
+ if("select_file") // Selecting new file will necessitate a change in paper type. This will be done on select_experiment and not here.
+ if(selected_file)
+ UnregisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED)
+ paper_to_be.set_experiment() // Clears the paper info.
+ for(var/datum/computer_file/data/ordnance/ordnance_data in holder.stored_files)
+ if(ordnance_data.uid == params["selected_uid"])
+ selected_file = ordnance_data
+ RegisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED, .proc/recheck_file_presence)
+ return TRUE
+ if("select_experiment")
+ var/ex_path = text2path(params["selected_expath"])
+ var/variable = selected_file.possible_experiments[text2path(params["selected_expath"])]
+ var/data = null
+ if(ispath(ex_path, /datum/experiment/ordnance/explosive))
+ paper_to_be = paper_to_be.clone_into(/datum/scientific_paper/explosive)
+ if(ispath(ex_path, /datum/experiment/ordnance/gaseous))
+ paper_to_be = paper_to_be.clone_into(/datum/scientific_paper/gaseous)
+ data = selected_file.return_data()
+ paper_to_be.set_experiment(ex_path, variable, data)
+ return TRUE
+ if("select_tier")
+ paper_to_be.set_tier(params["selected_tier"])
+ return TRUE
+ if("select_partner")
+ paper_to_be.set_partner(text2path(params["selected_partner"]), linked_techweb)
+ return TRUE
+ if("purchase_boost")
+ var/datum/scientific_partner/partner = locate(text2path(params["boost_seller"])) in SSresearch.scientific_partners
+ var/datum/techweb_node/node = SSresearch.techweb_node_by_id(params["purchased_boost"])
+ if(partner && node)
+ if(partner.purchase_boost(linked_techweb, node))
+ computer.say("Purchase succesful.")
+ playsound(computer, 'sound/machines/ping.ogg', 25)
+ return TRUE
+ playsound(computer, 'sound/machines/terminal_error.ogg', 25)
+ return FALSE
+
+/// Publication and adding points.
+/datum/computer_file/program/scipaper_program/proc/publish()
+ if(linked_techweb.add_scientific_paper(paper_to_be))
+ computer.say("\"[paper_to_be.title]\" has been published!")
+ paper_to_be = new
+ UnregisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED)
+ selected_file = null
+ SStgui.update_uis(src)
+ playsound(computer, 'sound/machines/ping.ogg', 25)
+ return TRUE
+ playsound(computer, 'sound/machines/terminal_error.ogg', 25)
+ return FALSE
diff --git a/code/modules/research/ordnance/tank_compressor.dm b/code/modules/research/ordnance/tank_compressor.dm
new file mode 100644
index 00000000000..b2061f45695
--- /dev/null
+++ b/code/modules/research/ordnance/tank_compressor.dm
@@ -0,0 +1,365 @@
+#define TANK_COMPRESSOR_PRESSURE_LIMIT 5000
+#define TANK_COMPRESSOR_MAX_TRANSFER_RATE 20
+#define SIGNIFICANT_AMOUNT_OF_MOLES 10
+
+/obj/machinery/atmospherics/components/binary/tank_compressor
+ name = "Tank Compressor"
+ desc = "Heavy duty shielded air compressor designed to pressurize tanks above the safe limit."
+ circuit = /obj/item/circuitboard/machine/tank_compressor
+ icon = 'icons/obj/machines/research.dmi'
+ base_icon_state = "tank_compressor"
+ icon_state = "tank_compressor-open"
+ density = TRUE
+
+ var/active = FALSE
+ var/transfer_rate = TANK_COMPRESSOR_MAX_TRANSFER_RATE
+ var/datum/gas_mixture/leaked_gas_buffer
+ var/list/compressor_record
+ var/last_recorded_pressure = 0
+ var/record_number = 1
+ var/obj/item/tank/inserted_tank
+ /// Reference to a disk we are going to print to.
+ var/obj/item/computer_hardware/hard_drive/portable/inserted_disk
+
+ pipe_flags = PIPING_ONE_PER_TURF | PIPING_DEFAULT_LAYER_ONLY
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/Initialize(mapload)
+ . = ..()
+ leaked_gas_buffer = new(200)
+ compressor_record = list()
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/ComponentInitialize()
+ . = ..()
+ RegisterSignal(src, COMSIG_ATOM_INTERNAL_EXPLOSION, .proc/explosion_handle)
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/examine()
+ . = ..()
+ . += "This one is rated for up to [TANK_COMPRESSOR_PRESSURE_LIMIT] kPa."
+
+/// Stores the record of the gas data for a significant enough tank leak
+/datum/data/compressor_record
+ /// Tank Name
+ var/experiment_source
+ /// Key: Path, Value: Moles
+ var/list/gas_data = list()
+ var/timestamp
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/attackby(obj/item/item, mob/living/user)
+ if (panel_open)
+ return ..()
+
+ if(istype(item, /obj/item/tank))
+ var/obj/item/tank/tank_item = item
+ if(inserted_tank)
+ if(!eject_tank(user))
+ balloon_alert(user, span_warning("[inserted_tank] is stuck inside."))
+ return ..()
+ if(!user.transferItemToLoc(tank_item, src))
+ balloon_alert(user, span_warning("[tank_item] is stuck to your hand."))
+ return ..()
+ inserted_tank = tank_item
+ last_recorded_pressure = 0
+ RegisterSignal(inserted_tank, COMSIG_PARENT_QDELETING, .proc/tank_destruction)
+ update_appearance()
+ return
+ if(istype(item, /obj/item/computer_hardware/hard_drive/portable))
+ var/obj/item/computer_hardware/hard_drive/portable/attacking_disk = item
+ eject_disk(user)
+ if(user.transferItemToLoc(attacking_disk, src))
+ inserted_disk = attacking_disk
+ else
+ balloon_alert(user, span_warning("[attacking_disk] is stuck to your hand."))
+ return
+ return ..()
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/wrench_act(mob/living/user, obj/item/tool)
+ if(active || inserted_tank)
+ return FALSE
+ if(!default_change_direction_wrench(user, tool))
+ return FALSE
+ return TRUE
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/screwdriver_act(mob/living/user, obj/item/tool)
+ if(active || inserted_tank)
+ return FALSE
+ if(!default_deconstruction_screwdriver(user, "[base_icon_state]-open", "[base_icon_state]-open", tool))
+ return FALSE
+ update_appearance()
+ return TRUE
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/crowbar_act(mob/living/user, obj/item/tool)
+ if(active || inserted_tank)
+ return FALSE
+ if(!default_deconstruction_crowbar(tool))
+ return FALSE
+ return TRUE
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/default_change_direction_wrench(mob/user, obj/item/wrench)
+ . = ..()
+ if(!.)
+ return
+
+ // Disconnect our partner.
+ if(nodes[1])
+ nodes[1].disconnect(src)
+ nodes[1] = null
+ if(parents[1])
+ nullify_pipenet(parents[1])
+ if(nodes[2])
+ nodes[2].disconnect(src)
+ nodes[2] = null
+ if(parents[2])
+ nullify_pipenet(parents[2])
+ set_init_directions()
+ // Connect to a new one.
+ atmos_init()
+ if(nodes[1])
+ nodes[1].atmos_init()
+ nodes[1].add_member(src)
+ if(nodes[2])
+ nodes[2].atmos_init()
+ nodes[2].add_member(src)
+ SSair.add_to_rebuild_queue(src)
+ update_appearance()
+
+/// Glorified volume pump.
+/obj/machinery/atmospherics/components/binary/tank_compressor/process_atmos()
+ var/datum/gas_mixture/input_air = airs[2]
+ if(!input_air?.total_moles() || !active || !transfer_rate || !inserted_tank)
+ return
+
+ var/datum/gas_mixture/tank_air = inserted_tank.return_air()
+ if(!tank_air)
+ return
+
+ if(input_air.return_pressure() < 0.01 || tank_air.return_pressure() > TANK_COMPRESSOR_PRESSURE_LIMIT)
+ return
+
+ /// Prevent pumping if tank is taking damage but still below pressure limit. Here to prevent exploiting the buffer system.
+ if((inserted_tank.leaking) && (tank_air.return_pressure() <= TANK_LEAK_PRESSURE))
+ active = FALSE
+ return
+
+ var/datum/gas_mixture/removed = input_air.remove_ratio(transfer_rate / input_air.volume)
+ if(!removed)
+ return
+
+ tank_air.merge(removed)
+ update_parents()
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/assume_air(datum/gas_mixture/giver)
+ if(!leaked_gas_buffer)
+ return ..()
+ leaked_gas_buffer.merge(giver)
+ return TRUE
+
+/// Recording of last pressure of the tank. Ran when a tank is about to explode or disintegrate. We dont care about last pressure if the tank is ejected.
+/obj/machinery/atmospherics/components/binary/tank_compressor/proc/tank_destruction()
+ SIGNAL_HANDLER
+ if(inserted_tank.get_integrity() > 0)
+ return
+ flush_buffer()
+ var/datum/gas_mixture/tank_air = inserted_tank.return_air()
+ last_recorded_pressure = tank_air.return_pressure()
+ active = FALSE
+ return
+
+/// Use this to absorb explosions.
+/obj/machinery/atmospherics/components/binary/tank_compressor/proc/explosion_handle(atom/source, list/arguments)
+ SIGNAL_HANDLER
+ say("Internal explosion detected and absorbed.")
+ SSexplosions.shake_the_room(get_turf(src), 1, 8, 0.5, 0.25, FALSE)
+ return COMSIG_CANCEL_EXPLOSION
+
+/**
+ * Everytime a tank is destroyed or a new tank is inserted, our buffer is flushed.
+ * Mole requirements in experiments are tracked by buffer data.
+ */
+/obj/machinery/atmospherics/components/binary/tank_compressor/proc/flush_buffer()
+ if(!leaked_gas_buffer.total_moles())
+ return
+ if(leaked_gas_buffer.total_moles() > SIGNIFICANT_AMOUNT_OF_MOLES)
+ record_data()
+ else
+ say("Buffer data discarded. Required moles for storage: [SIGNIFICANT_AMOUNT_OF_MOLES] moles.")
+ var/datum/gas_mixture/removed = leaked_gas_buffer.remove_ratio(1)
+ airs[1].merge(removed)
+ say("Gas stored in buffer flushed to output port. Compressor ready to start the next experiment.")
+
+/// This proc should be called whenever we want to store our buffer data.
+/obj/machinery/atmospherics/components/binary/tank_compressor/proc/record_data()
+ var/datum/data/compressor_record/new_record = new()
+ new_record.name = "Log Recording #[record_number]"
+ new_record.experiment_source = inserted_tank.name
+ new_record.timestamp = station_time_timestamp()
+ for(var/gas_path in leaked_gas_buffer.gases)
+ new_record.gas_data[gas_path] = leaked_gas_buffer.gases[gas_path][MOLES]
+
+ compressor_record += new_record
+ record_number += 1
+ say("Buffer data stored.")
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/proc/apply_experiments(datum/data/compressor_record/record)
+ var/list/passed_experiments = list()
+ var/list/gas_data = record.gas_data
+
+ for (var/datum/experiment/ordnance/gaseous/experiment in SSresearch.ordnance_experiments)
+ if(experiment.required_gas in gas_data)
+ if(gas_data[experiment.required_gas] > MINIMUM_MOLE_COUNT)
+ passed_experiments += list(experiment.type = gas_data[experiment.required_gas])
+
+ return passed_experiments
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/proc/print(mob/user, datum/data/compressor_record/record)
+ if(!record || !inserted_disk)
+ return
+
+ var/datum/computer_file/data/ordnance/gaseous/record_data = new
+ record_data.filename = "Gas Compressor " + record.name //Gas Compressor Log Recording #x
+ record_data.gas_record = record
+ record_data.possible_experiments = apply_experiments(record)
+
+ if(inserted_disk.store_file(record_data))
+ playsound(src, 'sound/machines/ping.ogg', 25)
+ else
+ playsound(src, 'sound/machines/terminal_error.ogg', 25)
+
+/// Ejecting a tank. Also called on insertion to clear previous tanks.
+/obj/machinery/atmospherics/components/binary/tank_compressor/proc/eject_tank(mob/user)
+ if(!inserted_tank)
+ return FALSE
+ var/datum/gas_mixture/tank_air = inserted_tank.return_air()
+ if(!tank_air.return_pressure() >= PUMP_MAX_PRESSURE)
+ return FALSE
+ flush_buffer()
+ if(user)
+ user.put_in_hands(inserted_tank)
+ else
+ inserted_tank.forceMove(drop_location())
+ active = FALSE
+ return TRUE
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/proc/eject_disk(mob/user)
+ if(!inserted_disk)
+ return FALSE
+ if(user)
+ user.put_in_hands(inserted_disk)
+ else
+ inserted_disk.forceMove(drop_location())
+ playsound(src, 'sound/machines/card_slide.ogg', 50)
+ return TRUE
+
+/// We rely on exited to clear references.
+/obj/machinery/atmospherics/components/binary/tank_compressor/Exited(atom/movable/gone, direction)
+ if(gone == inserted_disk)
+ inserted_disk = null
+ if(gone == inserted_tank)
+ UnregisterSignal(inserted_tank, COMSIG_PARENT_QDELETING)
+ inserted_tank = null
+ update_appearance()
+ . = ..()
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/on_deconstruction()
+ eject_tank()
+ eject_disk()
+ return ..()
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/Destroy()
+ inserted_tank = null
+ inserted_disk = null
+ leaked_gas_buffer = null
+ QDEL_NULL(compressor_record) //We only want the list nuked, not the contents.
+ return ..()
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/update_icon_state()
+ if(istype(inserted_tank))
+ icon_state = "[base_icon_state]-closed"
+ else
+ icon_state = "[base_icon_state]-open"
+ return ..()
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/update_overlays()
+ . = ..()
+ . += get_pipe_image(icon, "[base_icon_state]-pipe", dir, COLOR_BLUE, piping_layer)
+ . += get_pipe_image(icon, "[base_icon_state]-pipe", turn(dir, 180), COLOR_RED, piping_layer)
+ if(!istype(inserted_tank))
+ . += mutable_appearance(icon, "[base_icon_state]-doors",)
+ if(panel_open)
+ . += mutable_appearance(icon, "[base_icon_state]-maintenance")
+ else
+ . += mutable_appearance(icon, "[base_icon_state]-cables")
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/ui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "TankCompressor")
+ ui.open()
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/ui_act(action, list/params)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("change_rate")
+ transfer_rate = clamp(params["target"], 0, TANK_COMPRESSOR_MAX_TRANSFER_RATE)
+ if("toggle_injection")
+ active = !active
+ if("eject_tank")
+ eject_tank(usr)
+ if("eject_disk")
+ eject_disk(usr)
+ if("delete_record")
+ var/datum/data/compressor_record/record = locate(params["ref"]) in compressor_record
+ if(!compressor_record || !(record in compressor_record))
+ return
+ compressor_record -= record
+ return TRUE
+ if("print_record")
+ var/datum/data/compressor_record/record = locate(params["ref"]) in compressor_record
+ if(!compressor_record || !(record in compressor_record))
+ return
+ print(usr, record)
+ return TRUE
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/ui_static_data()
+ var/list/data = list(
+ "maxTransfer" = TANK_COMPRESSOR_MAX_TRANSFER_RATE,
+ "leakPressure" = round(TANK_LEAK_PRESSURE),
+ "fragmentPressure" = round(TANK_FRAGMENT_PRESSURE),
+ "ejectPressure" = PUMP_MAX_PRESSURE
+ )
+ return data
+
+/obj/machinery/atmospherics/components/binary/tank_compressor/ui_data(mob/user)
+ var/list/data = list()
+ data["tankPresent"] = inserted_tank ? TRUE : FALSE
+ var/datum/gas_mixture/tank_air = inserted_tank?.return_air()
+ data["tankPressure"] = tank_air?.return_pressure()
+ data["leaking"] = inserted_tank?.leaking
+
+ data["active"] = active
+ data["transferRate"] = transfer_rate
+ data["lastPressure"] = last_recorded_pressure
+
+ data["portData"] = list()
+ data["portData"] += list(gas_mixture_parser(airs[2], "Input Port"))
+ data["portData"] += list(gas_mixture_parser(airs[1], "Ouput Port"))
+ data["portData"] += list(gas_mixture_parser(leaked_gas_buffer, "Gas Buffer"))
+
+ data["disk"] = inserted_disk?.name
+ data["storage"] = "[inserted_disk?.used_capacity] / [inserted_disk?.max_capacity] GQ"
+ data["records"] = list()
+ for (var/datum/data/compressor_record/record in compressor_record)
+ var/list/single_record_data = list(
+ "ref" = REF(record),
+ "name" = record.name,
+ "source" = record.experiment_source,
+ "timestamp" = record.timestamp,
+ "gases" = list()
+ )
+ for (var/path in record.gas_data)
+ var/datum/gas/gas_path = path
+ single_record_data["gases"] += list(initial(gas_path.name) = record.gas_data[gas_path])
+ data["records"] += list(single_record_data)
+ return data
diff --git a/code/modules/research/techweb/_techweb.dm b/code/modules/research/techweb/_techweb.dm
index 351e26b980a..abccd710972 100644
--- a/code/modules/research/techweb/_techweb.dm
+++ b/code/modules/research/techweb/_techweb.dm
@@ -29,7 +29,6 @@
var/id = "generic"
/// IC logs
var/list/research_logs = list()
- var/largest_bomb_value = 0
/// Organization name, used for display
var/organization = "Third-Party"
/// Current per-second production, used for display only.
@@ -42,6 +41,18 @@
var/list/available_experiments = list()
/// Completed experiments
var/list/completed_experiments = list()
+
+ /**
+ * Assoc list of relationships with various partners
+ * scientific_cooperation[partner_typepath] = relationship
+ */
+ var/list/scientific_cooperation
+ /**
+ * Assoc list of papers already published by the crew.
+ * published_papers[experiment_typepath][tier] = paper
+ * Filled with nulls on init, populated only on publication.
+ */
+ var/list/published_papers
/datum/techweb/New()
SSresearch.techwebs += src
@@ -49,8 +60,8 @@
var/datum/techweb_node/DN = SSresearch.techweb_node_by_id(i)
research_node(DN, TRUE, FALSE, FALSE)
hidden_nodes = SSresearch.techweb_nodes_hidden.Copy()
+ initialize_published_papers()
return ..()
-
/datum/techweb/admin
id = "ADMIN"
organization = "CentCom"
@@ -337,15 +348,24 @@
researched_nodes -= node.id
recalculate_nodes(TRUE) //Fully rebuild the tree.
-/datum/techweb/proc/boost_with_path(datum/techweb_node/N, itempath)
- if(!istype(N) || !ispath(itempath))
+/// Boosts a techweb node.
+/datum/techweb/proc/boost_techweb_node(datum/techweb_node/node, list/pointlist)
+ if(!istype(node))
return FALSE
- LAZYINITLIST(boosted_nodes[N.id])
- for(var/i in N.boost_item_paths[itempath])
- boosted_nodes[N.id][i] = max(boosted_nodes[N.id][i], N.boost_item_paths[itempath][i])
- if(N.autounlock_by_boost)
- hidden_nodes -= N.id
- update_node_status(N)
+ LAZYINITLIST(boosted_nodes[node.id])
+ for(var/point_type in pointlist)
+ boosted_nodes[node.id][point_type] = max(boosted_nodes[node.id][point_type], pointlist[point_type])
+ if(node.autounlock_by_boost)
+ hidden_nodes -= node.id
+ update_node_status(node)
+ return TRUE
+
+/// Boosts a techweb node by using items.
+/datum/techweb/proc/boost_with_item(datum/techweb_node/node, itempath)
+ if(!istype(node) || !ispath(itempath))
+ return FALSE
+ var/list/boost_amount = node.boost_item_paths[itempath]
+ boost_techweb_node(node, boost_amount)
return TRUE
/datum/techweb/proc/update_tiers(datum/techweb_node/base)
@@ -478,3 +498,41 @@
/datum/techweb/specialized/autounlocking/exofab
allowed_buildtypes = MECHFAB
+
+/// Fill published_papers with nulls.
+/datum/techweb/proc/initialize_published_papers()
+ published_papers = list()
+ scientific_cooperation = list()
+ for (var/datum/experiment/ordnance/ordnance_experiment as anything in SSresearch.ordnance_experiments)
+ var/max_tier = min(length(ordnance_experiment.gain), length(ordnance_experiment.target_amount))
+ var/list/tier_list[max_tier]
+ published_papers[ordnance_experiment.type] = tier_list
+ for (var/datum/scientific_partner/partner as anything in SSresearch.scientific_partners)
+ scientific_cooperation[partner.type] = 0
+
+/// Publish the paper into our techweb. Cancel if we are not allowed to.
+/datum/techweb/proc/add_scientific_paper(datum/scientific_paper/paper_to_add)
+ if(!paper_to_add.allowed_to_publish(src))
+ return FALSE
+ paper_to_add.publish_paper(src)
+
+ // If we haven't published a paper in the same topic ...
+ if(locate(paper_to_add.experiment_path) in published_papers[paper_to_add.experiment_path])
+ return TRUE
+ // Quickly add and complete it.
+ // PS: It's also possible to use add_experiment() together with a list/available_experiments check
+ // to determine if we need to run all this, but this pretty much does the same while only needing one evaluation.
+
+ add_experiment(paper_to_add.experiment_path)
+
+ for (var/datum/experiment/experiment as anything in available_experiments)
+ if(experiment.type != paper_to_add.experiment_path)
+ continue
+
+ experiment.completed = TRUE
+ complete_experiment(experiment)
+ if(length(GLOB.experiment_handlers))
+ var/datum/component/experiment_handler/handler = GLOB.experiment_handlers[1]
+ handler.announce_message_to_all("The [experiment.name] has been completed!")
+
+ return TRUE
diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm
index 0245417d249..6ab7d68beba 100644
--- a/code/modules/research/techweb/_techweb_node.dm
+++ b/code/modules/research/techweb/_techweb_node.dm
@@ -72,21 +72,25 @@
unlock_ids -= node_id
/datum/techweb_node/proc/get_price(datum/techweb/host)
- if(host)
- var/list/actual_costs = research_costs.Copy()
- if(host.boosted_nodes[id])
- var/list/boostlist = host.boosted_nodes[id]
- for(var/booster in boostlist)
- if(actual_costs[booster])
- actual_costs[booster] -= boostlist[booster]
- for(var/cost_type in actual_costs)
- for(var/experiment_type in discount_experiments)
- if(host.completed_experiments[experiment_type]) //do we have this discount_experiment unlocked AND it wasn't applied already?
- actual_costs[cost_type] -= discount_experiments[experiment_type]
- return actual_costs
- else
+ if(!host)
return research_costs
+ var/list/actual_costs = research_costs.Copy()
+
+ for(var/cost_type in actual_costs)
+ for(var/experiment_type in discount_experiments)
+ if(host.completed_experiments[experiment_type]) //do we have this discount_experiment unlocked?
+ actual_costs[cost_type] -= discount_experiments[experiment_type]
+
+ if(host.boosted_nodes[id]) // Boosts should be subservient to experiments. Discount from boosts are capped when costs fall below 250.
+ var/list/boostlist = host.boosted_nodes[id]
+ for(var/booster in boostlist)
+ if(actual_costs[booster])
+ var/delta = max(0, actual_costs[booster] - 250)
+ actual_costs[booster] -= min(boostlist[booster], delta)
+
+ return actual_costs
+
/datum/techweb_node/proc/price_display(datum/techweb/TN)
return techweb_point_display_generic(get_price(TN))
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index a8329ceee74..420e2e61d58 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -935,6 +935,7 @@
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 5000)
discount_experiments = list(/datum/experiment/scanning/points/machinery_pinpoint_scan/tier3_microlaser = 4000)
+ required_experiments = list(/datum/experiment/ordnance/gaseous/noblium)
/////////////////////////Clown tech/////////////////////////
/datum/techweb_node/clown
@@ -1169,6 +1170,7 @@
"cybernetic_stomach_tier3",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 1500)
+ required_experiments = list(/datum/experiment/ordnance/gaseous/bz)
/datum/techweb_node/cyber_implants
id = "cyber_implants"
@@ -1198,6 +1200,7 @@
"ci-toolset",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+ required_experiments = list(/datum/experiment/ordnance/gaseous/nitrium)
/datum/techweb_node/combat_cyber_implants
id = "combat_cyber_implants"
@@ -1350,7 +1353,6 @@
"tele_shield",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 10000)
- required_experiments = list(/datum/experiment/explosion/calibration)
/datum/techweb_node/adv_weaponry
id = "adv_weaponry"
@@ -1361,7 +1363,7 @@
"pin_loyalty",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 10000)
- required_experiments = list(/datum/experiment/explosion/medium)
+ required_experiments = list(/datum/experiment/ordnance/explosive/highyieldbomb)
/datum/techweb_node/electric_weapons
id = "electronic_weapons"
@@ -1416,7 +1418,6 @@
"pyro_Grenade",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
- required_experiments = list(/datum/experiment/explosion/maxcap)
/datum/techweb_node/exotic_ammo
id = "exotic_ammo"
diff --git a/icons/obj/machines/research.dmi b/icons/obj/machines/research.dmi
index d834bbd32f5..2f3f0d43f43 100644
Binary files a/icons/obj/machines/research.dmi and b/icons/obj/machines/research.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 7ad8d4ca645..d875537e97c 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -662,6 +662,7 @@
#include "code\datums\components\area_sound_manager.dm"
#include "code\datums\components\areabound.dm"
#include "code\datums\components\armor_plate.dm"
+#include "code\datums\components\atmos_reaction_recorder.dm"
#include "code\datums\components\atmospheric_scanner.dm"
#include "code\datums\components\aura_healing.dm"
#include "code\datums\components\bakeable.dm"
@@ -1177,7 +1178,6 @@
#include "code\game\machinery\deployable.dm"
#include "code\game\machinery\dish_drive.dm"
#include "code\game\machinery\dna_scanner.dm"
-#include "code\game\machinery\doppler_array.dm"
#include "code\game\machinery\droneDispenser.dm"
#include "code\game\machinery\ecto_sniffer.dm"
#include "code\game\machinery\electrolyzer.dm"
@@ -2687,7 +2687,7 @@
#include "code\modules\experisci\experiment\types\dissection_experiment.dm"
#include "code\modules\experisci\experiment\types\experiment.dm"
#include "code\modules\experisci\experiment\types\exploration.dm"
-#include "code\modules\experisci\experiment\types\explosive.dm"
+#include "code\modules\experisci\experiment\types\ordnance.dm"
#include "code\modules\experisci\experiment\types\physical_experiment.dm"
#include "code\modules\experisci\experiment\types\random_scanning.dm"
#include "code\modules\experisci\experiment\types\scanning.dm"
@@ -3813,7 +3813,7 @@
#include "code\modules\research\server.dm"
#include "code\modules\research\stock_parts.dm"
#include "code\modules\research\anomaly\anomaly_core.dm"
-#include "code\modules\research\anomaly\explosive_compressor.dm"
+#include "code\modules\research\anomaly\anomaly_refinery.dm"
#include "code\modules\research\anomaly\raw_anomaly.dm"
#include "code\modules\research\designs\AI_module_designs.dm"
#include "code\modules\research\designs\autolathe_designs.dm"
@@ -3844,6 +3844,11 @@
#include "code\modules\research\machinery\departmental_techfab.dm"
#include "code\modules\research\machinery\protolathe.dm"
#include "code\modules\research\machinery\techfab.dm"
+#include "code\modules\research\ordnance\_scipaper.dm"
+#include "code\modules\research\ordnance\doppler_array.dm"
+#include "code\modules\research\ordnance\scipaper_partner.dm"
+#include "code\modules\research\ordnance\scipaper_publisher.dm"
+#include "code\modules\research\ordnance\tank_compressor.dm"
#include "code\modules\research\techweb\__techweb_helpers.dm"
#include "code\modules\research\techweb\_techweb.dm"
#include "code\modules\research\techweb\_techweb_node.dm"
diff --git a/tgui/packages/tgui/interfaces/AnomalyRefinery.js b/tgui/packages/tgui/interfaces/AnomalyRefinery.js
new file mode 100644
index 00000000000..3bd96534d59
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AnomalyRefinery.js
@@ -0,0 +1,220 @@
+import { useBackend, useSharedState } from '../backend';
+import {
+ Button,
+ Icon,
+ Modal,
+ Section,
+ Stack,
+ LabeledList,
+ Box,
+} from '../components';
+import { Window } from '../layouts';
+import { GasmixParser } from './common/GasmixParser';
+
+export const AnomalyRefinery = (props, context) => {
+ const { act, data } = useBackend(context);
+ return (
+
+
+
+
+
+ );
+};
+
+
+const AnomalyRefineryContent = (props, context) => {
+ const { act, data } = useBackend(context);
+ const [currentTab, changeTab] = useSharedState(context, 'exploderTab', 1);
+ const { core, valvePresent, active } = data;
+ return (
+
+ {currentTab === 1 && }
+ {currentTab === 2 && }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const CoreCompressorContent = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { core, requiredRadius, gasList, valveReady, active, valvePresent }
+ = data;
+ return (
+ <>
+
+ act('start_implosion')}
+ disabled={active || !valveReady || !core}>
+ {'Implode Core'}
+
+ }>
+ {!core && {'No Core Inserted!'}}
+
+ {core ? core : '-'}
+
+ {requiredRadius
+ ? requiredRadius + ' tiles'
+ : 'Implosion not possible.'}
+
+
+
+
+
+ act('swap')}>
+ {'Swap Merging Order'}
+
+ }>
+ {!valvePresent && {'No Bomb Inserted!'}}
+
+
+
+ {'Giver Tank ('
+ + (gasList[1].name ? gasList[1].name : 'Not Available')
+ + ')'}
+
+
+ {(gasList[1].total_moles
+ ? String(gasList[0].total_moles.toFixed(2))
+ : '-')
+ + ' moles at '
+ + (gasList[1].total_moles
+ ? String(gasList[1].temperature.toFixed(2))
+ : '-')
+ + ' Kelvin'}
+
+
+ {(gasList[1].total_moles
+ ? String(gasList[1].pressure.toFixed(2))
+ : '-') + ' kPa'}
+
+
+
+
+
+
+
+ {'Target Tank ('
+ + (gasList[0].name ? gasList[0].name : 'Not Available')
+ + ')'}
+
+
+ {(gasList[0].total_moles
+ ? String(gasList[0].total_moles.toFixed(2))
+ : '-')
+ + ' moles at '
+ + (gasList[0].total_moles
+ ? String(gasList[0].temperature.toFixed(2))
+ : '-')
+ + ' Kelvin'}
+
+
+ {(gasList[1].total_moles
+ ? String(gasList[0].pressure.toFixed(2))
+ : '-') + ' kPa'}
+
+
+
+
+
+ >
+ );
+};
+const BombProcessorContent = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { gasList, reactionIncrement } = data;
+ return (
+ <>
+
+ act('react')}
+ textAlign="center"
+ disabled={!gasList[0].total_moles || !gasList[1].total_moles}
+ content={reactionIncrement === 0 ? 'Open Valve' : 'React'} />
+ }>
+ {!gasList[2].total_moles && (
+ {'No Gas Present'}
+ )}
+
+
+
+
+
+ {[gasList[0], gasList[1]].map((individualGasmix) => (
+
+
+ {!individualGasmix.total_moles && (
+ {'No Gas Present'}
+ )}
+
+
+
+ ))}
+
+
+ >
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/DopplerArray.js b/tgui/packages/tgui/interfaces/DopplerArray.js
new file mode 100644
index 00000000000..d482ec78901
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/DopplerArray.js
@@ -0,0 +1,151 @@
+import { useBackend, useSharedState } from '../backend';
+import {
+ Box,
+ Button,
+ Flex,
+ LabeledList,
+ NoticeBox,
+ Section,
+ Stack,
+ Tabs,
+} from '../components';
+import { Window } from '../layouts';
+
+export const DopplerArray = (props, context) => {
+ const { act, data } = useBackend(context);
+ return (
+
+
+
+
+
+ );
+};
+
+const DopplerArrayContent = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { records = [], disk, storage } = data;
+ const [activeRecordName, setActiveRecordName] = useSharedState(
+ context,
+ 'activeRecordrecord',
+ records[0]?.name
+ );
+ const activeRecord = records.find((record) => {
+ return record.name === activeRecordName;
+ });
+ const DopplerArrayFooter = (
+
+
+ );
+ const DopplerArrayRecords = (
+
+
+
+
+ {records.map((record) => (
+ setActiveRecordName(record.name)}>
+ {record.name}
+
+ ))}
+
+
+ {activeRecord ? (
+
+
+
+ act('delete_record', {
+ 'ref': activeRecord.ref,
+ })}
+ />
+
+
+ ) : (
+
+ No Record Selected
+
+ )}
+
+
+ );
+ return (
+
+
+ {!records.length ? (
+ No Records
+ ) : (
+ DopplerArrayRecords
+ )}
+
+ {DopplerArrayFooter}
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/NtosScipaper.js b/tgui/packages/tgui/interfaces/NtosScipaper.js
new file mode 100644
index 00000000000..961c3098fc6
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/NtosScipaper.js
@@ -0,0 +1,385 @@
+import { useBackend } from '../backend';
+import {
+ BlockQuote,
+ Button,
+ Collapsible,
+ Dropdown,
+ Input,
+ LabeledList,
+ Section,
+ Stack,
+ Tabs,
+ Box,
+ Table,
+ NoticeBox,
+ Tooltip,
+ Icon,
+} from '../components';
+import { TableCell, TableRow } from '../components/Table';
+import { NtosWindow } from '../layouts';
+
+export const NtosScipaper = (props, context) => {
+ return (
+
+
+
+
+
+ );
+};
+
+const PaperPublishing = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ title,
+ author,
+ etAlia,
+ abstract,
+ fileList = [],
+ expList = [],
+ allowedTiers = [],
+ allowedPartners = [],
+ gains,
+ selectedFile,
+ selectedExperiment,
+ tier,
+ selectedPartner,
+ coopIndex,
+ fundingIndex,
+ } = data;
+ return (
+ <>
+
+
+
+
+ act('rewrite', {
+ title: value,
+ })}
+ />
+
+
+
+ act('rewrite', {
+ author: value,
+ })}
+ />
+
+
+
+
+ act('rewrite', {
+ abstract: value,
+ })}
+ />
+
+
+
+
+
+ act('select_file', {
+ selected_uid: fileList[ordfile_name],
+ })}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ act('select_experiment', {
+ selected_expath: expList[experiment_name],
+ })}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ String(number))}
+ displayText={tier ? String(tier) : '-'}
+ onSelected={(new_tier) =>
+ act('select_tier', {
+ selected_tier: Number(new_tier),
+ })}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ act('select_partner', {
+ selected_partner: allowedPartners[new_partner],
+ })}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {' Cooperation: '}
+ {gains[coopIndex - 1]}
+
+
+
+
+
+ {' Funding: '}
+ {gains[fundingIndex - 1]}
+
+
+
+
+ >
+ );
+};
+
+const PaperBrowser = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { publishedPapers, coopIndex, fundingIndex } = data;
+ if (publishedPapers.length === 0) {
+ return No Published Papers! ;
+ } else {
+ return publishedPapers.map((paper) => (
+
+
+
+
+ {paper['experimentName'] + ' - ' + paper['tier']}
+
+
+ {paper['author'] + (paper.etAlia ? ' et al.' : '')}
+
+
+ {paper['partner']}
+
+
+
+
+ {paper['gains'][coopIndex - 1]}
+
+
+ {paper['gains'][fundingIndex - 1]}
+
+
+
+
+ {paper['abstract']}
+
+
+
+
+ ));
+ }
+};
+const ExperimentBrowser = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { experimentInformation = [] } = data;
+ return experimentInformation.map((experiment) => (
+
+ {experiment.description}
+
+
+ {Object.keys(experiment.target).map((tier) => (
+
+ {experiment.target[tier] + ' ' + experiment.suffix}
+
+ ))}
+
+
+ ));
+};
+
+const PartnersBrowser = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ partnersInformation,
+ coopIndex,
+ fundingIndex,
+ purchaseableBoosts = [],
+ relations = [],
+ visibleNodes = [],
+ } = data;
+ return partnersInformation.map((partner) => (
+
+
+
+
+ {partner.flufftext}
+
+
+ {relations[partner.path]}
+
+
+ {partner.multipliers[coopIndex - 1] + 'x'}
+
+
+ {partner.multipliers[fundingIndex - 1] + 'x'}
+
+
+ {partner.acceptedExperiments.map((experiment_name) => (
+ {experiment_name}
+ ))}
+
+
+
+ {partner.boostedNodes.map((node) => (
+
+
+ {visibleNodes.includes(node.id)
+ ? node.name
+ : 'Unknown Technology'}
+
+
+
+
+ ))}
+
+
+
+
+
+ ));
+};
+
+export const NtosScipaperContent = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { currentTab } = data;
+ return (
+ <>
+
+
+ act('change_tab', {
+ new_tab: 1,
+ })}>
+ {'Publish Papers'}
+
+
+ act('change_tab', {
+ new_tab: 2,
+ })}>
+ {'View Previous Publications'}
+
+
+ act('change_tab', {
+ new_tab: 3,
+ })}>
+ {'View Available Experiments'}
+
+
+ act('change_tab', {
+ new_tab: 4,
+ })}>
+ {'View Scientific Partners'}
+
+
+ {currentTab === 1 && }
+ {currentTab === 2 && }
+ {currentTab === 3 && }
+ {currentTab === 4 && }
+ >
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/TachyonArray.js b/tgui/packages/tgui/interfaces/TachyonArray.js
deleted file mode 100644
index 87c3395edfa..00000000000
--- a/tgui/packages/tgui/interfaces/TachyonArray.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import { useBackend, useSharedState } from '../backend';
-import { Button, Flex, LabeledList, NoticeBox, Section, Tabs } from '../components';
-import { Window } from '../layouts';
-
-export const TachyonArray = (props, context) => {
- const { act, data } = useBackend(context);
- const {
- records = [],
- is_research,
- } = data;
- return (
-
-
-
-
- {!records.length ? (
-
- No Records
-
- ) : (
-
- )}
-
- {!!is_research && (
-
-
-
- )}
-
-
-
- );
-};
-
-export const TachyonArrayContent = (props, context) => {
- const { act, data } = useBackend(context);
- const {
- records = [],
- } = data;
- const [
- activeRecordName,
- setActiveRecordName,
- ] = useSharedState(context, 'record', records[0]?.name);
- const activeRecord = records.find(record => {
- return record.name === activeRecordName;
- });
- return (
-
-
-
-
- {records.map(record => (
- setActiveRecordName(record.name)}>
- {record.name}
-
- ))}
-
-
- {activeRecord ? (
-
-
- act('delete_record', {
- 'ref': activeRecord.ref,
- })} />
-
-
- ) : (
-
-
- No Record Selected
-
-
- )}
-
-
- );
-};
diff --git a/tgui/packages/tgui/interfaces/TankCompressor.js b/tgui/packages/tgui/interfaces/TankCompressor.js
new file mode 100644
index 00000000000..417f15b0d85
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/TankCompressor.js
@@ -0,0 +1,327 @@
+import { useBackend, useSharedState } from '../backend';
+import {
+ Box,
+ Button,
+ Flex,
+ Icon,
+ Modal,
+ RoundGauge,
+ Section,
+ Slider,
+ Stack,
+ NoticeBox,
+ Tabs,
+ LabeledList,
+} from '../components';
+import { Window } from '../layouts';
+import { GasmixParser } from './common/GasmixParser';
+
+export const TankCompressor = (props, context) => {
+ const { act, data } = useBackend(context);
+ return (
+
+
+
+
+
+ );
+};
+
+const TankCompressorContent = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { disk, storage } = data;
+ const [currentTab, changeTab] = useSharedState(context, 'compressorTab', 1);
+ return (
+
+ {currentTab === 1 && }
+ {currentTab === 2 && }
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const AlertBoxes = (props, context) => {
+ const { text_content, icon_name, icon_break, color, active } = props;
+ const { act, data } = useBackend(context);
+ return (
+
+
+
+
+ {icon_break &&
}
+ {text_content}
+
+
+
+ );
+};
+
+const TankCompressorControls = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ tankPresent,
+ leaking,
+ lastPressure,
+ leakPressure,
+ fragmentPressure,
+ tankPressure,
+ maxTransfer,
+ active,
+ transferRate,
+ ejectPressure,
+ portData = [],
+ } = data;
+ const pressure = tankPresent ? tankPressure : lastPressure;
+ const usingLastData = !!(lastPressure && !tankPresent);
+ return (
+ <>
+
+ ejectPressure}
+ onClick={() => act('eject_tank')}>
+ {'Eject Tank'}
+
+ }>
+ {!pressure && {'No Pressure Detected'}}
+ {usingLastData && (
+
+ {'Tank destroyed. Displaying last recorded data.'}
+
+ )}
+
+
+ (value ? value.toFixed(2) : '-') + ' kPa'}
+ />
+
+
+
+
+
+ = leakPressure}
+ />
+
+
+
+
+ = leakPressure
+ && pressure < fragmentPressure)
+ || leaking
+ }
+ />
+
+
+ = fragmentPressure}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ act('change_rate', { target: new_rate })}
+ />
+
+
+
+
+
+
+
+
+
+ {portData.map((individualGasmix) => (
+
+
+ {!individualGasmix.total_moles && (
+ {'No Gas Present'}
+ )}
+
+
+
+ ))}
+
+
+ >
+ );
+};
+
+const TankCompressorRecords = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { records = [], disk } = data;
+ const [activeRecordRef, setActiveRecordRef] = useSharedState(
+ context,
+ 'recordRef',
+ records[0]?.ref
+ );
+ const activeRecord = !!activeRecordRef && records.find((record) =>
+ activeRecordRef === record.ref);
+ if (records.length === 0) {
+ return (
+
+ No Records
+ );
+ }
+ else {
+ return (
+
+
+
+
+ {records.map((record) => (
+ setActiveRecordRef(record.ref)}>
+ {record.name}
+
+ ))}
+
+
+ {activeRecord ? (
+
+ {
+ act('delete_record', {
+ 'ref': activeRecord.ref,
+ });
+ }}
+ />,
+
+
+ ) : (
+
+ No Record Selected
+
+ )}
+
+
+ );
+ }
+};
diff --git a/tgui/packages/tgui/interfaces/ThermoMachine.js b/tgui/packages/tgui/interfaces/ThermoMachine.js
index 87043b8cd26..ee044bc3329 100644
--- a/tgui/packages/tgui/interfaces/ThermoMachine.js
+++ b/tgui/packages/tgui/interfaces/ThermoMachine.js
@@ -27,7 +27,7 @@ export const ThermoMachine = (props, context) => {
average: [.65, .825],
bad: [.4225, .64],
}}>
- {Math.round(data.efficiency * 10000)/100 + " %"}
+ {(data.efficiency * 100).toFixed(2) + " %"}
);
diff --git a/tgui/packages/tgui/interfaces/common/GasmixParser.js b/tgui/packages/tgui/interfaces/common/GasmixParser.js
new file mode 100644
index 00000000000..26b37b2724f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/common/GasmixParser.js
@@ -0,0 +1,25 @@
+
+import { useBackend } from '../../backend';
+import { LabeledList } from '../../components';
+
+export const GasmixParser = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { gases=[], temperature, volume, pressure, total_moles } = props;
+ return (
+
+ {Object.keys(gases).map((gas_name) => (
+
+ {gases[gas_name].toFixed(2)
+ + ' mol ('
+ + ((gases[gas_name] / total_moles) * 100).toFixed(2)
+ + ' %)'}
+
+ ))}
+
+ {(total_moles ? temperature.toFixed(2) : "-")+ ' K'}
+
+ {(total_moles ? volume.toFixed(2) : "-") + ' L'}
+ {(total_moles ? pressure.toFixed(2) : "-") + ' kPa'}
+
+ );
+};