diff --git a/code/__DEFINES/research/anomalies.dm b/code/__DEFINES/research/anomalies.dm
new file mode 100644
index 0000000000..35d99a982d
--- /dev/null
+++ b/code/__DEFINES/research/anomalies.dm
@@ -0,0 +1,21 @@
+// Anomaly core types
+/// Bluespace cores
+#define ANOMALY_CORE_BLUESPACE /obj/item/assembly/signaler/anomaly/bluespace
+/// Gravitational cores
+#define ANOMALY_CORE_GRAVITATIONAL /obj/item/assembly/signaler/anomaly/grav
+/// Flux
+#define ANOMALY_CORE_FLUX /obj/item/assembly/signaler/anomaly/flux
+/// Vortex
+#define ANOMALY_CORE_VORTEX /obj/item/assembly/signaler/anomaly/vortex
+/// Pyro
+#define ANOMALY_CORE_PYRO /obj/item/assembly/signaler/anomaly/pyro
+
+// Max amounts of cores you can make
+#define MAX_CORES_BLUESPACE 8
+#define MAX_CORES_GRAVITATIONAL 8
+#define MAX_CORES_FLUX 8
+#define MAX_CORES_VORTEX 8
+#define MAX_CORES_PYRO 8
+
+/// chance supermatter anomalies drop real cores
+#define SUPERMATTER_ANOMALY_DROP_CHANCE 20
diff --git a/code/controllers/subsystem/research.dm b/code/controllers/subsystem/research.dm
index 60c5dd5b99..ac9db60ccc 100644
--- a/code/controllers/subsystem/research.dm
+++ b/code/controllers/subsystem/research.dm
@@ -294,6 +294,17 @@ SUBSYSTEM_DEF(research)
//[88nodes * 5000points/node] / [1.5hr * 90min/hr * 60s/min]
//Around 450000 points max???
+ /// The global list of raw anomaly types that have been refined, for hard limits.
+ var/list/created_anomaly_types = list()
+ /// The hard limits of cores created for each anomaly type. For faster code lookup without switch statements.
+ var/list/anomaly_hard_limit_by_type = list(
+ ANOMALY_CORE_BLUESPACE = MAX_CORES_BLUESPACE,
+ ANOMALY_CORE_PYRO = MAX_CORES_PYRO,
+ ANOMALY_CORE_GRAVITATIONAL = MAX_CORES_GRAVITATIONAL,
+ ANOMALY_CORE_VORTEX = MAX_CORES_VORTEX,
+ ANOMALY_CORE_FLUX = MAX_CORES_FLUX
+ )
+
/datum/controller/subsystem/research/Initialize()
point_types = TECHWEB_POINT_TYPE_LIST_ASSOCIATIVE_NAMES
initialize_all_techweb_designs()
diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm
index 2b7411d03f..b82d932e6d 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -551,4 +551,4 @@ Class Procs:
AM.pixel_y = -8 + (round( . / 3)*8)
/obj/machinery/rust_heretic_act()
- take_damage(500, BRUTE, "melee", 1)
+ take_damage(500, BRUTE, "melee", 1)
diff --git a/code/game/objects/effects/anomalies.dm b/code/game/objects/effects/anomalies.dm
index 871fe838d7..7e3a5205de 100644
--- a/code/game/objects/effects/anomalies.dm
+++ b/code/game/objects/effects/anomalies.dm
@@ -17,11 +17,15 @@
var/countdown_colour
var/obj/effect/countdown/anomaly/countdown
-/obj/effect/anomaly/Initialize(mapload, new_lifespan)
+ /// chance we drop a core when neutralized
+ var/core_drop_chance = 100
+
+/obj/effect/anomaly/Initialize(mapload, new_lifespan, core_drop_chance = 100)
. = ..()
GLOB.poi_list |= src
START_PROCESSING(SSobj, src)
impact_area = get_area(src)
+ src.core_drop_chance = core_drop_chance
if (!impact_area)
return INITIALIZE_HINT_QDEL
@@ -54,6 +58,8 @@
GLOB.poi_list.Remove(src)
STOP_PROCESSING(SSobj, src)
qdel(countdown)
+ if(aSignal)
+ QDEL_NULL(aSignal)
return ..()
/obj/effect/anomaly/proc/anomalyEffect()
@@ -70,12 +76,12 @@
/obj/effect/anomaly/proc/anomalyNeutralize()
new /obj/effect/particle_effect/smoke/bad(loc)
- for(var/atom/movable/O in src)
- O.forceMove(drop_location())
+ if(prob(core_drop_chance))
+ aSignal.forceMove(drop_location())
+ aSignal = null
qdel(src)
-
/obj/effect/anomaly/attackby(obj/item/I, mob/user, params)
if(I.tool_behaviour == TOOL_ANALYZER) //revert if runtimed
to_chat(user, "Analyzing... [src]'s unstable field is fluctuating along frequency [format_frequency(aSignal.frequency)], code [aSignal.code].")
diff --git a/code/game/objects/items/circuitboards/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machine_circuitboards.dm
index 806aed92ed..8e59e91e38 100644
--- a/code/game/objects/items/circuitboards/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machine_circuitboards.dm
@@ -1146,3 +1146,8 @@
build_path = /obj/machinery/atmospherics/components/unary/shuttle/heater
req_components = list(/obj/item/stock_parts/micro_laser = 2,
/obj/item/stock_parts/matter_bin = 1)
+
+/obj/item/circuitboard/machine/explosive_compressor
+ name = "Explosive Compressor (Machine Board)"
+ build_path = /obj/machinery/research/explosive_compressor
+ req_components = list(/obj/item/stock_parts/matter_bin = 3)
diff --git a/code/game/objects/items/devices/transfer_valve.dm b/code/game/objects/items/devices/transfer_valve.dm
index 32d9c02a27..8b0d46be44 100644
--- a/code/game/objects/items/devices/transfer_valve.dm
+++ b/code/game/objects/items/devices/transfer_valve.dm
@@ -289,3 +289,9 @@
. = TRUE
update_icon()
+
+/**
+ * Returns if this is ready to be detonated. Checks if both tanks are in place.
+ */
+/obj/item/transfer_valve/proc/ready()
+ return tank_one && tank_two
diff --git a/code/modules/cargo/packs/science.dm b/code/modules/cargo/packs/science.dm
index fd6fee362d..125bfe2034 100644
--- a/code/modules/cargo/packs/science.dm
+++ b/code/modules/cargo/packs/science.dm
@@ -192,3 +192,49 @@
crate_type = /obj/structure/closet/crate/secure/science
dangerous = TRUE
+//////// RAW ANOMALY CORES
+
+/datum/supply_pack/science/raw_flux_anomaly
+ name = "Raw Flux Anomaly"
+ desc = "The raw core of a flux anomaly, ready to be implosion-compressed into a powerful artifact."
+ cost = 5000
+ access = ACCESS_TOX
+ contains = list(/obj/item/raw_anomaly_core/flux)
+ crate_name = "raw flux anomaly"
+ crate_type = /obj/structure/closet/crate/secure/science
+
+/datum/supply_pack/science/raw_grav_anomaly
+ name = "Raw Gravitational Anomaly"
+ desc = "The raw core of a gravitational anomaly, ready to be implosion-compressed into a powerful artifact."
+ cost = 5000
+ access = ACCESS_TOX
+ contains = list(/obj/item/raw_anomaly_core/grav)
+ crate_name = "raw pyro anomaly"
+ crate_type = /obj/structure/closet/crate/secure/science
+
+/datum/supply_pack/science/raw_vortex_anomaly
+ name = "Raw Vortex Anomaly"
+ desc = "The raw core of a vortex anomaly, ready to be implosion-compressed into a powerful artifact."
+ cost = 5000
+ access = ACCESS_TOX
+ contains = list(/obj/item/raw_anomaly_core/vortex)
+ crate_name = "raw vortex anomaly"
+ crate_type = /obj/structure/closet/crate/secure/science
+
+/datum/supply_pack/science/raw_bluespace_anomaly
+ name = "Raw Bluespace Anomaly"
+ desc = "The raw core of a bluespace anomaly, ready to be implosion-compressed into a powerful artifact."
+ cost = 5000
+ access = ACCESS_TOX
+ contains = list(/obj/item/raw_anomaly_core/bluespace)
+ crate_name = "raw bluespace anomaly"
+ crate_type = /obj/structure/closet/crate/secure/science
+
+/datum/supply_pack/science/raw_pyro_anomaly
+ name = "Raw Pyro Anomaly"
+ desc = "The raw core of a pyro anomaly, ready to be implosion-compressed into a powerful artifact."
+ cost = 5000
+ access = ACCESS_TOX
+ contains = list(/obj/item/raw_anomaly_core/pyro)
+ crate_name = "raw pyro anomaly"
+ crate_type = /obj/structure/closet/crate/secure/science
diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm
index d4e3f6ad0d..e3fdbd3654 100644
--- a/code/modules/power/supermatter/supermatter.dm
+++ b/code/modules/power/supermatter/supermatter.dm
@@ -639,7 +639,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
if(produces_gas)
env.merge(removed)
air_update_turf()
-
+
/*********
END CITADEL CHANGES
*********/
@@ -1033,12 +1033,12 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
if(L)
switch(type)
if(FLUX_ANOMALY)
- var/obj/effect/anomaly/flux/A = new(L, 300, FALSE)
+ var/obj/effect/anomaly/flux/A = new(L, 300, SUPERMATTER_ANOMALY_DROP_CHANCE)
A.explosive = FALSE
if(GRAVITATIONAL_ANOMALY)
- new /obj/effect/anomaly/grav(L, 250, FALSE)
+ new /obj/effect/anomaly/grav(L, 250, SUPERMATTER_ANOMALY_DROP_CHANCE)
if(PYRO_ANOMALY)
- new /obj/effect/anomaly/pyro(L, 200, FALSE)
+ new /obj/effect/anomaly/pyro(L, 200, SUPERMATTER_ANOMALY_DROP_CHANCE)
/obj/machinery/power/supermatter_crystal/proc/supermatter_zap(atom/zapstart = src, range = 5, zap_str = 4000, zap_flags = ZAP_SUPERMATTER_FLAGS, list/targets_hit = list())
if(QDELETED(zapstart))
diff --git a/code/modules/research/anomaly/explosive_compressor.dm b/code/modules/research/anomaly/explosive_compressor.dm
new file mode 100644
index 0000000000..23983e31dc
--- /dev/null
+++ b/code/modules/research/anomaly/explosive_compressor.dm
@@ -0,0 +1,152 @@
+#define MAX_RADIUS_REQUIRED 8000 //tritbomb
+#define MIN_RADIUS_REQUIRED 20 //maxcap
+/**
+ * # Explosive compressor machines
+ *
+ * The explosive compressor machine used in anomaly core production.
+ *
+ * Uses the standard toxins/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
+ circuit = /obj/item/circuitboard/machine/explosive_compressor
+
+ /// 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 last time we did say_requirements(), because someone will inevitably click spam this.
+ var/last_requirements_say = 0
+
+/obj/machinery/research/explosive_compressor/examine(mob/user)
+ . = ..()
+ . += "Ctrl-Click to remove an inserted core."
+ . += "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/attack_hand(mob/living/user)
+ . = ..()
+ if(.)
+ return
+ if(!inserted_core)
+ to_chat(user, "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, "There is no core inserted.")
+ return
+ inserted_core.forceMove(get_turf(user))
+ to_chat(user, "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/C)
+ var/required = get_required_radius(C.anomaly_type)
+ if(isnull(required))
+ say("Unfortunately, due to diminishing supplies of condensed anomalous matter, [C] and any cores of its type are no longer of a sufficient quality level to be compressed into a working core.")
+ else
+ say("[C] 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/I, mob/living/user, params)
+ if(default_unfasten_wrench(user, I))
+ return
+ if(default_deconstruction_screwdriver(user, "explosive_compressor", "explosive_compressor", I))
+ return
+ if(default_deconstruction_crowbar(I))
+ return
+ . = ..()
+ if(istype(I, /obj/item/raw_anomaly_core))
+ if(inserted_core)
+ to_chat(user, "There is already a core in [src].")
+ return
+ if(!user.transferItemToLoc(I, src))
+ to_chat(user, "[I] is stuck to your hand.")
+ return
+ inserted_core = I
+ to_chat(user, "You insert [I] into [src].")
+ return
+ if(istype(I, /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, "There is no core inserted in [src]. What would be the point of detonating an implosion without a core?")
+ return
+ var/obj/item/transfer_valve/valve = I
+ if(!valve.ready())
+ to_chat(user, "[valve] is incomplete.")
+ return
+ if(!user.transferItemToLoc(I, src))
+ to_chat(user, "[I] is stuck to your hand.")
+ return
+ inserted_bomb = I
+ to_chat(user, "You insert [I] and press the start button.")
+ do_implosion()
+
+/**
+ * The ""explosion"" proc.
+ */
+/obj/machinery/research/explosive_compressor/proc/do_implosion()
+ var/required_radius = get_required_radius(inserted_core.anomaly_type)
+ // By now, we should be sure that we have a core, a TTV, and that the TTV has both tanks in place.
+ var/datum/gas_mixture/mix1 = inserted_bomb.tank_one.air_contents
+ var/datum/gas_mixture/mix2 = inserted_bomb.tank_two.air_contents
+ // Snowflaked tank explosion
+ var/datum/gas_mixture/mix = new(70) // Standard tank volume, 70L
+ mix.merge(mix1)
+ mix.merge(mix2)
+ mix.react()
+ if(mix.return_pressure() < TANK_FRAGMENT_PRESSURE)
+ // They failed so miserably we're going to give them their bomb back.
+ inserted_bomb.forceMove(drop_location())
+ inserted_bomb = null
+ inserted_core.forceMove(drop_location())
+ inserted_core = null
+ say("Transfer valve resulted in negligible explosive power. Items ejected.")
+ return
+ mix.react() // build more pressure
+ var/pressure = mix.return_pressure()
+ var/range = (pressure - TANK_FRAGMENT_PRESSURE) / TANK_FRAGMENT_SCALE
+ QDEL_NULL(inserted_bomb) // bomb goes poof
+ if(range < required_radius)
+ inserted_bomb.forceMove(src)
+ say("Resultant detonation failed to produce enough implosive power to compress [inserted_core]. Core ejected.")
+ return
+ inserted_core.create_core(drop_location(), TRUE, TRUE)
+ inserted_core = null
+ say("Success. Resultant detonation has theoretical range of [range]. Required radius was [required_radius]. Core production complete.")
+
+#undef MAX_RADIUS_REQUIRED
+#undef MIN_RADIUS_REQUIRED
diff --git a/code/modules/research/anomaly/raw_anomaly.dm b/code/modules/research/anomaly/raw_anomaly.dm
new file mode 100644
index 0000000000..2342edd28f
--- /dev/null
+++ b/code/modules/research/anomaly/raw_anomaly.dm
@@ -0,0 +1,73 @@
+/**
+ * # Raw Anomaly Cores
+ *
+ * The current precursor to anomaly cores, these are manufactured into 'finished' anomaly cores for use in research, items, and more.
+ *
+ * The current amounts created is stored in SSresearch.created_anomaly_types[ANOMALY_CORE_TYPE_DEFINE] = amount
+ * The hard limits are in code/__DEFINES/anomalies.dm
+ */
+/obj/item/raw_anomaly_core
+ name = "raw anomaly core"
+ desc = "You shouldn't be seeing this. Someone screwed up."
+ icon = 'icons/obj/assemblies/new_assemblies.dmi'
+ icon_state = "broken_state"
+
+ /// Anomaly type
+ var/anomaly_type
+
+/obj/item/raw_anomaly_core/bluespace
+ name = "raw bluespace core"
+ desc = "The raw core of a bluespace anomaly, glowing and full of potential."
+ anomaly_type = /obj/item/assembly/signaler/anomaly/bluespace
+ icon_state = "rawcore_bluespace"
+
+/obj/item/raw_anomaly_core/vortex
+ name = "raw vortex core"
+ desc = "The raw core of a vortex anomaly. Feels heavy to the touch."
+ anomaly_type = /obj/item/assembly/signaler/anomaly/vortex
+ icon_state = "rawcore_vortex"
+
+/obj/item/raw_anomaly_core/grav
+ name = "raw gravity core"
+ desc = "The raw core of a gravity anomaly. The air seems attracted to it."
+ anomaly_type = /obj/item/assembly/signaler/anomaly/grav
+ icon_state = "rawcore_grav"
+
+/obj/item/raw_anomaly_core/pyro
+ desc = "The raw core of a pyro anomaly. It is warm to the touch."
+ name = "raw pyro core"
+ anomaly_type = /obj/item/assembly/signaler/anomaly/pyro
+ icon_state = "rawcore_pyro"
+
+/obj/item/raw_anomaly_core/flux
+ name = "raw flux core"
+ desc = "The raw core of a flux anomaly, faintly crackling with energy."
+ anomaly_type = /obj/item/assembly/signaler/anomaly/flux
+ icon_state = "rawcore_flux"
+
+/obj/item/raw_anomaly_core/random
+ name = "random raw core"
+ desc = "You should not see this!"
+ icon_state = "rawcore_bluespace"
+
+/obj/item/raw_anomaly_core/random/Initialize(mapload)
+ . = ..()
+ var/path = pick(subtypesof(/obj/item/raw_anomaly_core))
+ new path(loc)
+ return INITIALIZE_HINT_QDEL
+
+/**
+ * Created the resulting core after being "made" into it.
+ *
+ * Arguments:
+ * * newloc - Where the new core will be created
+ * * del_self - should we qdel(src)
+ * * count_towards_limit - should we increment the amount of created cores on SSresearch
+ */
+/obj/item/raw_anomaly_core/proc/create_core(newloc, del_self = FALSE, count_towards_limit = FALSE)
+ . = new anomaly_type(newloc)
+ if(count_towards_limit)
+ var/existing = SSresearch.created_anomaly_types[anomaly_type] || 0
+ SSresearch.created_anomaly_types[anomaly_type] = existing + 1
+ if(del_self)
+ qdel(src)
diff --git a/tgstation.dme b/tgstation.dme
index 2b31b68243..aeb3ba1d54 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -142,6 +142,7 @@
#include "code\__DEFINES\mapping\maploader.dm"
#include "code\__DEFINES\material\worth.dm"
#include "code\__DEFINES\mobs\slowdowns.dm"
+#include "code\__DEFINES\research\anomalies.dm"
#include "code\__DEFINES\research\stock_parts.dm"
#include "code\__DEFINES\skills\defines.dm"
#include "code\__DEFINES\skills\helpers.dm"
@@ -3143,6 +3144,8 @@
#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\raw_anomaly.dm"
#include "code\modules\research\designs\AI_module_designs.dm"
#include "code\modules\research\designs\autobotter_designs.dm"
#include "code\modules\research\designs\autoylathe_designs.dm"