diff --git a/Content.Server/Singularity/Components/RadiationCollectorComponent.cs b/Content.Server/Singularity/Components/RadiationCollectorComponent.cs index fee2c4c729..e958c7dab7 100644 --- a/Content.Server/Singularity/Components/RadiationCollectorComponent.cs +++ b/Content.Server/Singularity/Components/RadiationCollectorComponent.cs @@ -1,4 +1,7 @@ using Content.Server.Singularity.EntitySystems; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Singularity.Components { @@ -33,5 +36,55 @@ namespace Content.Server.Singularity.Components /// Timestamp when machine can be deactivated again. /// public TimeSpan CoolDownEnd; + + /// + /// List of gases that will react to the radiation passing through the collector + /// + [DataField("radiationReactiveGases")] + [ViewVariables(VVAccess.ReadWrite)] + public List? RadiationReactiveGases; + } + + /// + /// Describes how a gas reacts to the collected radiation + /// + [DataDefinition] + public sealed partial class RadiationReactiveGas + { + /// + /// The reactant gas + /// + [DataField("reactantPrototype", required: true)] + public Gas Reactant = Gas.Plasma; + + /// + /// Multipier for the amount of power produced by the radiation collector when using this gas + /// + [DataField("powerGenerationEfficiency")] + public float PowerGenerationEfficiency = 1f; + + /// + /// Controls the rate (molar percentage per rad) at which the reactant breaks down when exposed to radiation + /// + /// /// + /// Set to zero if the reactant does not deplete + /// + [DataField("reactantBreakdownRate")] + public float ReactantBreakdownRate = 1f; + + /// + /// A byproduct gas that is generated when the reactant breaks down + /// + /// + /// Leave null if the reactant no byproduct gas is to be formed + /// + [DataField("byproductPrototype")] + public Gas? Byproduct = null; + + /// + /// The molar ratio of the byproduct gas generated from the reactant gas + /// + [DataField("molarRatio")] + public float MolarRatio = 1f; } } diff --git a/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs b/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs index 142686fcd0..8fa05386e2 100644 --- a/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs @@ -4,9 +4,12 @@ using Content.Shared.Singularity.Components; using Content.Server.Popups; using Content.Server.Power.Components; using Content.Shared.Radiation.Events; -using Robust.Server.GameObjects; using Robust.Shared.Timing; -using Robust.Shared.Player; +using Robust.Shared.Containers; +using Content.Server.Atmos.Components; +using Content.Shared.Examine; +using Content.Server.Atmos; +using System.Diagnostics.CodeAnalysis; namespace Content.Server.Singularity.EntitySystems { @@ -15,19 +18,36 @@ namespace Content.Server.Singularity.EntitySystems [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnRadiation); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnAnalyzed); + } + + private bool TryGetLoadedGasTank(EntityUid uid, [NotNullWhen(true)] out GasTankComponent? gasTankComponent) + { + gasTankComponent = null; + var container = _containerSystem.EnsureContainer(uid, "GasTank"); + + if (container.ContainedEntity == null) + return false; + + if (!EntityManager.TryGetComponent(container.ContainedEntity, out gasTankComponent)) + return false; + + return true; } private void OnInteractHand(EntityUid uid, RadiationCollectorComponent component, InteractHandEvent args) { var curTime = _gameTiming.CurTime; - if(curTime < component.CoolDownEnd) + if (curTime < component.CoolDownEnd) return; ToggleCollector(uid, args.User, component); @@ -36,7 +56,37 @@ namespace Content.Server.Singularity.EntitySystems private void OnRadiation(EntityUid uid, RadiationCollectorComponent component, OnIrradiatedEvent args) { - if (!component.Enabled) return; + if (!component.Enabled || component.RadiationReactiveGases == null) + return; + + if (!TryGetLoadedGasTank(uid, out var gasTankComponent)) + return; + + var charge = 0f; + + foreach (var gas in component.RadiationReactiveGases) + { + float reactantMol = gasTankComponent.Air.GetMoles(gas.Reactant); + float delta = args.TotalRads * reactantMol * gas.ReactantBreakdownRate; + + // We need to offset the huge power gains possible when using very cold gases + // (they allow you to have a much higher molar concentrations of gas in the tank). + // Hence power output is modified using the Michaelis-Menten equation, + // it will heavily penalise the power output of low temperature reactions: + // 300K = 100% power output, 73K = 49% power output, 1K = 1% power output + float temperatureMod = 1.5f * gasTankComponent.Air.Temperature / (150f + gasTankComponent.Air.Temperature); + charge += args.TotalRads * reactantMol * component.ChargeModifier * gas.PowerGenerationEfficiency * temperatureMod; + + if (delta > 0) + { + gasTankComponent.Air.AdjustMoles(gas.Reactant, -Math.Min(delta, reactantMol)); + } + + if (gas.Byproduct != null) + { + gasTankComponent.Air.AdjustMoles((int) gas.Byproduct, delta * gas.MolarRatio); + } + } // No idea if this is even vaguely accurate to the previous logic. // The maths is copied from that logic even though it works differently. @@ -45,15 +95,39 @@ namespace Content.Server.Singularity.EntitySystems // This still won't stop things being potentially hilariously unbalanced though. if (TryComp(uid, out var batteryComponent)) { - var charge = args.TotalRads * component.ChargeModifier; batteryComponent.CurrentCharge += charge; } } + private void OnExamined(EntityUid uid, RadiationCollectorComponent component, ExaminedEvent args) + { + if (!TryGetLoadedGasTank(uid, out var gasTankComponent)) + { + args.PushMarkup(Loc.GetString("power-radiation-collector-gas-tank-missing")); + return; + } + + args.PushMarkup(Loc.GetString("power-radiation-collector-gas-tank-present")); + + if (gasTankComponent.IsLowPressure) + { + args.PushMarkup(Loc.GetString("power-radiation-collector-gas-tank-low-pressure")); + } + } + + private void OnAnalyzed(EntityUid uid, RadiationCollectorComponent component, GasAnalyzerScanEvent args) + { + if (!TryGetLoadedGasTank(uid, out var gasTankComponent)) + return; + + args.GasMixtures = new Dictionary { { Name(uid), gasTankComponent.Air } }; + } + public void ToggleCollector(EntityUid uid, EntityUid? user = null, RadiationCollectorComponent? component = null) { if (!Resolve(uid, ref component)) return; + SetCollectorEnabled(uid, !component.Enabled, user, component); } @@ -61,6 +135,7 @@ namespace Content.Server.Singularity.EntitySystems { if (!Resolve(uid, ref component)) return; + component.Enabled = enabled; // Show message to the player @@ -68,7 +143,6 @@ namespace Content.Server.Singularity.EntitySystems { var msg = component.Enabled ? "radiation-collector-component-use-on" : "radiation-collector-component-use-off"; _popupSystem.PopupEntity(Loc.GetString(msg), uid); - } // Update appearance diff --git a/Resources/Locale/en-US/power/components/radiation-collector.ftl b/Resources/Locale/en-US/power/components/radiation-collector.ftl new file mode 100644 index 0000000000..d68296fbea --- /dev/null +++ b/Resources/Locale/en-US/power/components/radiation-collector.ftl @@ -0,0 +1,3 @@ +power-radiation-collector-gas-tank-missing = [color=red]No gas tank attached.[/color] +power-radiation-collector-gas-tank-present = A gas tank is [color=darkgreen]connected[/color]. +power-radiation-collector-gas-tank-low-pressure = The gas tank [color=orange]low pressure[/color] light is on. \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/Fills/Crates/engines.yml b/Resources/Prototypes/Catalog/Fills/Crates/engines.yml index 11e0224207..99f937b1e3 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/engines.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/engines.yml @@ -42,7 +42,7 @@ components: - type: StorageFill contents: - - id: RadiationCollector + - id: RadiationCollectorFullTank - type: entity id: CrateEngineeringSingularityContainment diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/collector.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/collector.yml index 9fe95c7c27..ecdc3b3fbc 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/collector.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/collector.yml @@ -1,7 +1,8 @@ - type: entity id: RadiationCollector name: radiation collector - description: A machine that collects radiation and turns it into power. + suffix: Empty tank + description: A machine that collects radiation and turns it into power. Requires plasma gas to function. placement: mode: SnapgridCenter components: @@ -38,6 +39,12 @@ !type:CableDeviceNode nodeGroupID: HVPower - type: RadiationCollector + chargeModifier: 7500 + radiationReactiveGases: + - reactantPrototype: Plasma + powerGenerationEfficiency: 1 + reactantBreakdownRate: 0.0002 + byproductPrototype: Tritium # Note that this doesn't matter too much (see next comment) # However it does act as a cap on power receivable via the collector. - type: Battery @@ -55,3 +62,38 @@ supplyRampTolerance: 1000000000 - type: GuideHelp guides: [ Singularity, Power ] + - type: ContainerContainer + containers: + GasTank: !type:ContainerSlot {} + - type: ItemSlots + slots: + GasTank: + startingItem: PlasmaTank + whitelist: + components: + - GasTank + +- type: entity + id: RadiationCollectorNoTank + suffix: No tank + parent: RadiationCollector + components: + - type: ItemSlots + slots: + GasTank: + whitelist: + components: + - GasTank + +- type: entity + id: RadiationCollectorFullTank + suffix: Filled tank + parent: RadiationCollector + components: + - type: ItemSlots + slots: + GasTank: + startingItem: PlasmaTankFilled + whitelist: + components: + - GasTank \ No newline at end of file diff --git a/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml b/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml index 76115fb437..5ebcd0d7d6 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml @@ -28,8 +28,11 @@ Emitter lasers and containment field can cause damage, avoid touching them when ## Radition collectors + -They connect to HV cables and generate power from nearby radiation sources when turned on. +They connect to HV cables and generate power from nearby radiation sources when turned on. +Radiation collectors require a tank full of gaseous plasma in order to operate. +Continous radiation exposure will gradually convert the stored plasma into tritium, so replace depleted plasma tanks with fresh ones to maintain a high power output. ## Particle accelerator