From eeb546593148ce940e9adac2c663c453d6557247 Mon Sep 17 00:00:00 2001
From: vincentiusvin <54709710+vincentiusvin@users.noreply.github.com>
Date: Thu, 3 Mar 2022 18:05:37 +0700
Subject: [PATCH] Ordnance Content Update: Scientific Papers (#62284)
How do I play/test/operate this?
Download NT Frontier on any modular computers. It should debrief you on what experiments are available and how to publish.
If you want to do a bomb experiment, make sure it's captured by the doppler array (as usual) and then print the experiments into a disk and publish it.
If you want to do a gas experiment, make the gas and either pump it into a tank and 1) overpressurize it with a "clear" gas like N2 or 2) overpressurize tanks with the gas itself. Make sure you do the overpressurizing in the compressor machine. When tanks are destroyed/ejected leaked gas will get recorded. Print it into a disk and publish it.
For publication, the file needs to be directly present inside the computer's HDD. This means you need to copy it first with the file manager.
Fill the data (if desired, it will autofill with boiler plate if you dont) and send away!
Doing experiments unlock nodes, while doing them well unlocks boosts (which are discounts but slightly more restrictive) which are purchaseable with NT Frontier.
If you are testing this and have access to admin tools, there are various premade bombs under obj/effect/spawner/newbomb
A doc I wrote detailing the why and what part of this PR.
https://hackmd.io/JOakSYVMSh2zU2YL5ju_-Q
---
# Intro
## The Problem(s)
Ordnance, (previously toxins) seems to lack a lot of content and things to do. The gameplay loop consists of making a bomb and then sending it off for credits or using it to refine cores. Ordnance at it's inception originally relies on players experimenting and finding the perfect mix over multiple rounds, but once the recipe for a "do-everything" mix got out, the original charm of individual discoveries becomes meaningless.
Another issue with ordnance is the odd difficulty curve. As a new player, ordnance is almost impossible to decipher, but once you watch a tutorial or read a wiki and can mail a 50k into space, there pretty much isn't anything else to do. Most players will be satisfied at this point without the gameplay loop encouraging them to understand or play more. The only thing you can do afterwards is to sink your teeth in and understand why that particular mix explodes the way it does. This again has a significant difficulty curve, but if you do that, the department doesn't acknowledge or reward that in any way. There are pretty much two huge spikes, with the latter one not really existing inside the department.
TLDR:
* The content being same-y over rounds.
* Odd difficulty curve:
1. A new player is oblivious to everything.
2. Those in the middle can repeat the final goal consistently without needing to understanding why
3. There is nothing to justify spending more time in the department after reaching the midgame.
## Abstract
Scientific Papers aim to add a framework to run multiple experiments in ordnance. Adding more experiments scattered across various atmospheric aspects might allow players of various knowledge levels to still have something engaging to do. A new player should have an easier challange than to mail a 50K. While those that already can make bombs should have an easier time understanding why their bombs explode the way it does. Once they fully understand why, they can set their sights on taking advantage of another reaction to set their bomb off or hone one particular reaction down.
## Goals
* Have some intro-level challanges for new players.
* Have some semblance of late-game challanges for more experienced players.
* Explain the mechanics better for those in the middle of the road.
* Incentivize trying new things out in the department.
* Better integrate Ordnance with Experisci
## Boundaries / Dont's
* Do not incentivize people to learn ordnance by using PvP loots.
* Do not shake or change the reaction system by a huge amount.
* Disincentivize having a single god-mix that does everything.
****
# Main design pillars
## A. Framework surrounding the experiments
### A.1. New experiments
Add new experiments to the ExperiSci module. These will come in two flavours: New explosions to do, and various gas synthesis experiments. Both of these are actually supported by the map layout of ordnance right now, but there is no reason to do anything outside of making a 50k as fast as possible.
### A.2. Rewards for experiments: Cash and Techweb Boosts.
Scientific papers will add a separate experiment handling system. A single experiment will be graded into various tiers, each tier corresponding to the explosion size or amount of gas made. Doing any tier of a specific experiment will unlock the discount for that specific reactions. A single explosion **WILL NOT** do multiple experiments (or even tiers) at once.
On publication, a partner can be selected. A single partner only has a specific criteria of experiments they want. The experiments will then be graded on "how good they are done", with the criteria being more punishing as tier increases. Publication will then reward scientific cooperation with the partnered partner. Players can spend this cooperation on techweb boosts. Techweb boosts are meant to be subservient to discount from experiments and will not shave a node's price to be lower than 500 points.
**Experiments will only unlock nodes, discounts are handled through this boost system.**
This is more for maintainability than anything.
### A.3. On Tedium
*This is a note on implementation more than anything, but I think this helps explains why several things are done.*
Due to the nature of atmospheric reactions in the game (they're all linear), tedium is a very important thing to consider. An experiment should have a sweet spot to aim for, but there should not be a point where further mastery is stopped dead on it's track with a reward cap.
Scientific Papers attempts to discourage this behaviour by having the "maximum score" scale off to infinity but with the rewards being smaller and smaller. The sweet spot is always there to aim for and should be well communicated with players, but on their last submission of an experiment topic players should be encouraged to do their best. There should always be a reward for pushing the system to it's limit as long as it doesn't completely nullify the other subdepartments. This is the reason why there is a hard limit on the number of publications and why the score calculation is a bit more complex than it needed to be.
## B. Gas Synthesis (Early-Mid Game)
Scientific papers will add one new machine that requests a tank to release x amounts of y gas. This will be accomplished by adding a tank pumping machine which will either burst or explode a tank, releasing the gas inside. The gas currently requested are BZ, Nitryl, Halon and Nob.
The overarching goal of this compressor machine is to present a gas synthesis challange for the players and to get them more accustomed to how a tank explodes. The gas synthesis part can always be changed in order to reflect the current state of atmospheric reactions.
## C. Explosion Changes (Mid-Late Game)
### C.1 Cause and effect.
The main theme of the explosion changes is establishing cause and effect of explosions. Reactions that happens inside a tank that's going to explode will be recorded and forwarded to a doppler array. Some experiments will require only a single cause to be present (think of it as isolating a variable). This is currently implemented for nobliumformation and pressure based bombs. Having other reactions occuring besides noblium formation will fail the first one, while having any reactions at all will fail the second one.
Adding more explosions here will be a slight challange because as of now the game has only two reactions that can reliably make an explosion.
### C.2 Tools upgrade.
Doppler array has now been retrofitted to state the probable cause of an explosion, be it reactions or just overpressurization on gas merging. These should help intermediate players figure out what is causing an explosion.
Added a new functionality to the implosion compressor:
Basically performs the gas merging and reaction that TTV does in a machine and reports the results back as if someone uses an analyzer on them. Here to give players feedback so they can try and understand what is actually going on in a bomb.
## D. Player Interaction
There should be more room for more than 1 player to play ordnance simultaneously. Previously players are also able to split tasks, but this rarely happens because tritium synthesis needs only the gas chamber to be reconfigured. Now, different players can pick different experiments and work on them. Players can also do joint tasks on one single experiment. Gases like noblium will need tritium production and also a cooling module online.
Ordnance can also coordinate with their parent department on what they really need, be it money or research bonuses.
# Potential Changes
The best-case changes that can be implemented if the current roster of content isn't enough is more reactions that can be used in bombs. Eliminating bombs entirely goes against the spirit of the subdepartment, while adding new ones will need a lot of care and consideration.
Another possible change is to implement a "gas payload" bomb. Bombs that has a set number of unreacting gas inside that will increase the heat capacity, reduce the payload, and neccesitates more bespoke mixes.
Adding more gas synthesis experiments is discouraged. The main focus of ordnance should be bombs, with gas synthesis being a side project for ordnance. These are present to ease the introduction to bombs and provide some side content.
There should be a somewhat well-justified goal in adding new synthesis experiments: e.g. BZ is there as a "tutorial" gas, Nitryl to introduce players to cooling/heating mixes, Halon to a more efficient tritium production, and Nob as a nudge to nobformation bombs and mastery over other aspects.
# Conclusion / Summary
Add more experiments to ordnance that players can take, accomplish this by:
1. Making the players perform gas synthesis or make bombs.
2. Have them collect the data, see if it fits the criteria. Explain why if it fits and why if it doesn't.
3. Have the player publish a paper.
Reward them based on how well did they do, give players agency both on the experiment phase and also publication phase.
---
TLDR: Added new experiment to toxins, added the framework for those experiments existing. Experiments comes in gas synthesis and also bombs but with more parameters. Experiments needs to be published through papers, various choices to be made there.
Implementation notes:
Because of how paper works, ordnance experiments are handled outside of experiment_handler components. My reasoning for this is twofold:
The experiments will be completed manually on publication and if the experiment isn't unlocked yet it will still be completed.
Experiment handler datums have several procs which require an atom-level parent, and I figured this is the most sensible and cleanest way to implement this without changing the experiment handler datum too much.
Small change to /obj/machinery/proc/power_change() signal ordering to adjust the state first and then send the signal. Didn't found any other usage of this signal except mine but barge down my door if it broke something.
Rewrote the ttv merge_gases() code to be slightly more readable.
A small code improvement for thermomachine to use tofixed (my fault).
Ordnance have been updated to enable the publication of papers
Several new explosive and gas synthesis experiments have been added to ordnance
Anomaly compressor has been TGUIzed and now supports simulating the reaction of the gases inside the ttv.
New tank compressor machine for toxins. You can overpressurize tanks with exotic gases and complete experiments.
Several techweb nodes are locked and require toxin experiments to complete.
Toxins can purchase boosts for various techweb nodes.
You no longer need to anchor doppler arrays for it to work.
Doppler array and implosion compressor now supports deconstruction, implosion compressor construction added.
Doppler now emits a red light to denote it's direction and it being on. Doppler not malf.
Implosion compressor renamed to anomaly refinery.
Created a new program tab "Science" for the downloader app. Removed Robotics.
Reworked the code for bombspawner (used in the cuban pete arcade game)
---
.../map_files/Deltastation/DeltaStation2.dmm | 41 +-
.../map_files/IceBoxStation/IceBoxStation.dmm | 48 ++-
_maps/map_files/KiloStation/KiloStation.dmm | 71 ++--
_maps/map_files/MetaStation/MetaStation.dmm | 263 +++++++-----
_maps/map_files/tramstation/tramstation.dmm | 43 +-
code/__DEFINES/atmospherics/atmos_piping.dm | 7 +
.../signals_atom/signals_atom_explosion.dm | 3 +-
code/__DEFINES/dcs/signals/signals_datum.dm | 18 +
code/__DEFINES/machines.dm | 2 +-
code/__DEFINES/research.dm | 11 -
code/__HELPERS/atmospherics.dm | 35 ++
code/controllers/subsystem/air.dm | 3 +
code/controllers/subsystem/explosions.dm | 7 +-
code/controllers/subsystem/research.dm | 20 +
.../components/atmos_reaction_recorder.dm | 59 +++
code/game/machinery/computer/arcade/arcade.dm | 2 +-
code/game/machinery/doppler_array.dm | 270 ------------
.../objects/effects/spawners/bombspawner.dm | 148 +++++--
.../circuitboards/machine_circuitboards.dm | 21 +-
.../objects/items/devices/transfer_valve.dm | 43 +-
code/game/objects/items/tanks/tanks.dm | 32 +-
.../atmospherics/environmental/LINDA_fire.dm | 21 +-
.../atmospherics/gasmixtures/gas_mixture.dm | 6 +-
.../atmospherics/gasmixtures/reactions.dm | 154 ++++---
.../experisci/experiment/experiments.dm | 95 ++++-
.../experiment/handlers/experiment_handler.dm | 15 -
.../experisci/experiment/types/explosive.dm | 39 --
.../experisci/experiment/types/ordnance.dm | 48 +++
.../computers/machinery/console_presets.dm | 2 +-
.../modular_computers/file_system/data.dm | 28 +-
.../file_system/programs/airestorer.dm | 2 +-
.../file_system/programs/borg_monitor.dm | 4 +-
.../file_system/programs/ntdownloader.dm | 2 +-
.../file_system/programs/ntnrc_client.dm | 8 +-
.../file_system/programs/robocontrol.dm | 2 +-
.../file_system/programs/robotact.dm | 2 +-
.../file_system/programs/techweb.dm | 1 +
.../modular_computers/hardware/hard_drive.dm | 6 +
code/modules/power/power.dm | 9 +-
.../research/anomaly/anomaly_refinery.dm | 314 ++++++++++++++
.../research/anomaly/explosive_compressor.dm | 221 ----------
code/modules/research/destructive_analyzer.dm | 2 +-
code/modules/research/experimentor.dm | 2 +-
code/modules/research/ordnance/_scipaper.dm | 355 ++++++++++++++++
.../research/ordnance/doppler_array.dm | 315 ++++++++++++++
.../research/ordnance/scipaper_partner.dm | 47 +++
.../research/ordnance/scipaper_publisher.dm | 227 +++++++++++
.../research/ordnance/tank_compressor.dm | 365 +++++++++++++++++
code/modules/research/techweb/_techweb.dm | 78 +++-
.../modules/research/techweb/_techweb_node.dm | 30 +-
code/modules/research/techweb/all_nodes.dm | 7 +-
icons/obj/machines/research.dmi | Bin 26364 -> 26895 bytes
tgstation.dme | 11 +-
.../tgui/interfaces/AnomalyRefinery.js | 220 ++++++++++
tgui/packages/tgui/interfaces/DopplerArray.js | 151 +++++++
tgui/packages/tgui/interfaces/NtosScipaper.js | 385 ++++++++++++++++++
tgui/packages/tgui/interfaces/TachyonArray.js | 133 ------
.../tgui/interfaces/TankCompressor.js | 327 +++++++++++++++
.../packages/tgui/interfaces/ThermoMachine.js | 2 +-
.../tgui/interfaces/common/GasmixParser.js | 25 ++
60 files changed, 3739 insertions(+), 1069 deletions(-)
create mode 100644 code/datums/components/atmos_reaction_recorder.dm
delete mode 100644 code/game/machinery/doppler_array.dm
delete mode 100644 code/modules/experisci/experiment/types/explosive.dm
create mode 100644 code/modules/experisci/experiment/types/ordnance.dm
create mode 100644 code/modules/research/anomaly/anomaly_refinery.dm
delete mode 100644 code/modules/research/anomaly/explosive_compressor.dm
create mode 100644 code/modules/research/ordnance/_scipaper.dm
create mode 100644 code/modules/research/ordnance/doppler_array.dm
create mode 100644 code/modules/research/ordnance/scipaper_partner.dm
create mode 100644 code/modules/research/ordnance/scipaper_publisher.dm
create mode 100644 code/modules/research/ordnance/tank_compressor.dm
create mode 100644 tgui/packages/tgui/interfaces/AnomalyRefinery.js
create mode 100644 tgui/packages/tgui/interfaces/DopplerArray.js
create mode 100644 tgui/packages/tgui/interfaces/NtosScipaper.js
delete mode 100644 tgui/packages/tgui/interfaces/TachyonArray.js
create mode 100644 tgui/packages/tgui/interfaces/TankCompressor.js
create mode 100644 tgui/packages/tgui/interfaces/common/GasmixParser.js
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 d834bbd32f5a37c354930984f44a0d13a07af3cd..2f3f0d43f43d48fa7c528895d278ce4fd629b045 100644
GIT binary patch
literal 26895
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
literal 26364
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