Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<IsPackable>false</IsPackable>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2026 Goob Station Contributors
//
// SPDX-License-Identifier: MPL-2.0

using Content.Goobstation.Shared.Supermatter.Systems;

namespace Content.Goobstation.Client.Supermatter.Systems;

public sealed class SupermatterSystem : SharedSupermatterSystem
{
public override void Initialize()
{
base.Initialize();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: 2026 Goob Station Contributors
//
// SPDX-License-Identifier: MPL-2.0

using Content.Goobstation.Shared.Supermatter.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Systems;
using Content.Shared.Atmos;
using Content.Shared.Chat;
using Robust.Shared.Maths;
namespace Content.Goobstation.Server.Supermatter;

/// <summary>
/// <para>Simple <see cref="IDisposable"/> wrapper around <see cref="GasMixture"/></para>
/// <para>Splits off a part of gas, and merges them together later</para>
/// <para><see cref="AtmosphereSystem.Merge(GasMixture, GasMixture)"/> is automatically called at the end -
/// use <see cref="GasMixture"/> instead if you want to handle it manually</para>
/// </summary>
readonly struct GasWrapper(GasMixture surroundingMix, float ratio, AtmosphereSystem atmosphere) : IDisposable
{
private readonly GasMixture _surrounding = surroundingMix;

/// <summary>
/// The split off part of your gas.
/// </summary>
public readonly GasMixture Gas = surroundingMix.RemoveRatio(ratio);

public void Dispose()
{
atmosphere.Merge(_surrounding, Gas);
}
}

public static class SmExtensions
{
/// <summary>
/// Get SM related data about a provided gas mix.
/// </summary>
/// <param name="absorbedGas">Mix to be parsed</param>
/// <returns>A selection of values, check <see cref="SupermatterComponent.GasDataFields(Gas)"/></returns>
public static (float radModifier, float zapModifier, float moleModifier, float heatModifier, float heatResistModifier) GetGasModifiers(this GasMixture absorbedGas)
{
var totalMoles = absorbedGas.TotalMoles;

// Safety check: Prevent a divide-by-zero NaN cascade if the mix is completely empty
if (totalMoles <= 0f)
{
return (1f, 1f, 1f, 1f, 1f);
}

var radModifier = 1f;
var zapModifier = 1f;
var moleModifier = 1f;
var heatModifier = 1f;
var heatResistModifier = 1f;

// Safely iterate through the actual enum values, regardless of their integer backing
foreach (Gas gas in Enum.GetValues<Gas>())
{
var proportion = absorbedGas.GetGasMolarPercentage(gas);

// Skip doing math if there's none of this gas in the mix
if (proportion <= 0f) continue;

var facts = SupermatterComponent.GasDataFields(gas);

radModifier += proportion * facts.RadMod;
zapModifier += proportion * facts.ZapMod;
moleModifier += proportion * facts.MoleMod;
heatModifier += proportion * facts.HeatMod;
heatResistModifier += proportion * facts.HeatResistMod;
}

// Ensure we don't do something stupid later
return (
Math.Max(radModifier, 0f),
Math.Max(zapModifier, 0f),
Math.Max(moleModifier, 0f),
Math.Max(heatModifier, 0f),
Math.Max(heatResistModifier, 0f)
);
}

public static float GetGasMolarPercentage(this GasMixture gasMix, Gas gas)
{
if (!(gasMix.TotalMoles > 0f))
return 0f;
return gasMix.GetMoles(gas) / gasMix.TotalMoles;
}

public static float GetGasMolarPercentage(this GasMixture gasMix, int gas)
{
if (!(gasMix.TotalMoles > 0f))
return 0f;
return gasMix.GetMoles(gas) / gasMix.TotalMoles;
}

/// <summary>
/// Help the SM announce something.
/// </summary>
/// <param name="global">If true, does the station announcement.</param>
/// <param name="customSender">If true, sends the announcement from Central Command.</param>
public static void SupermatterAnnouncement(this ChatSystem chat, EntityUid uid, string message, bool global = false, string? customSender = null)
{
if (global)
{
var sender = customSender ?? Loc.GetString("supermatter-announcer");
chat.DispatchStationAnnouncement(uid, message, sender, colorOverride: Color.Yellow);
return;
}
chat.TrySendInGameICMessage(uid, message, InGameICChatType.Speak, hideChat: false, checkRadioPrefix: true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: 2026 Goob Station Contributors
//
// SPDX-License-Identifier: MPL-2.0

using Content.Goobstation.Shared.Supermatter.Components;
using Content.Goobstation.Shared.Supermatter.Systems;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Radiation.Components;

namespace Content.Goobstation.Server.Supermatter.Systems;

public sealed partial class SupermatterSystem : SharedSupermatterSystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;

/// <summary>
/// Handle power and radiation output depending on atmospheric things.
/// </summary>
private void ProcessAtmos(EntityUid uid, SupermatterComponent sm)
{
#region Get gas mix

var mix = _atmosphere.GetContainingMixture(uid, true, true);

if (mix is not { })
return;

using var absorbed = new GasWrapper(mix, sm.GasEfficiency, _atmosphere);

var moles = absorbed.Gas.TotalMoles;

if (!(moles > 0f))
return;

#endregion

var (radModifier, zapModifier, moleModifier, heatModifier, heatResistModifier) = absorbed.Gas.GetGasModifiers();

#region Calculate CO2 powerloss inhibition effect

var co2Ratio = absorbed.Gas.GetGasMolarPercentage(Gas.CarbonDioxide);
var underThresholdScaler = Math.Min(
Math.Clamp(co2Ratio / sm.PowerlossInhibitionGasThreshold, 0, 1),
Math.Clamp(moles / sm.PowerlossInhibitionMoleThreshold, 0, 1)
);

// Apply CO2 ratio if thresholds are met, otherwise limit the ratio according to how far away we are from thresholds
sm.PowerlossDynamicScaling = co2Ratio * underThresholdScaler;

//
var moleBoost = Math.Clamp(moles / sm.PowerlossInhibitionMoleBoostThreshold, 1f, 1.5f);
var powerlossInhibitor = Math.Clamp(1f - sm.PowerlossDynamicScaling * moleBoost, 0f, 1f);

#endregion

#region Add power to crystal

// Transfer matter power to power
if (sm.MatterPower != 0)
{
// Get how much matter power to transfer
var removedMatter = Math.Clamp(sm.MatterPower, 0f, 1f * sm.MatterPowerConversion);

sm.Power = Math.Max(sm.Power + removedMatter, 0);
sm.MatterPower = Math.Max(sm.MatterPower - removedMatter, 0);
}

// Increase power from temperature
sm.Power = Math.Max(absorbed.Gas.Temperature * heatModifier / Atmospherics.T0C + sm.Power, 0);

// Yeah, it consumes all ammonia in one tick cuz it's funny af
sm.Power = Math.Max(absorbed.Gas.GetMoles(Gas.Ammonia) * sm.AmmoniaEnergyPerMole + sm.Power, 0);
absorbed.Gas.SetMoles(Gas.Ammonia, 0f);

#endregion

#region Generate outputs

//Radiate stuff
if (TryComp<RadiationSourceComponent>(uid, out var rad))
{
rad.Intensity = sm.Power * radModifier * sm.RadiationOutputFactor;
}

// Convert power to energy
var energy = sm.Power * sm.ReactionPowerModifier;

// Release the waste. Both are scaled by modifier and energy, but o2 also scales with temperatures.
absorbed.Gas.AdjustMoles(Gas.Oxygen, Math.Max(moleModifier * (energy + absorbed.Gas.Temperature - Atmospherics.T0C) * sm.OxygenReleaseEfficiencyModifier, 0f));
absorbed.Gas.AdjustMoles(Gas.Plasma, Math.Max(moleModifier * sm.PlasmaReleaseModifier * energy, 0f));

// Increase temperature
absorbed.Gas.Temperature += energy * sm.ThermalReleaseModifier;

#endregion

#region Scale down power

// I'd recommend plotting these two if you want to get it
// but in general this lets it need less input to stay under 10 power than above
// Below 10 power it substracts very little, and above it substracts 1/10
// 10f (and 0.9f) hardcoded to discourage yaml majors messing with it since it impacts a lot
// (And would require massive structural changes, all to minuscule benefit)
var powerReduction = (float)Math.Pow(sm.Power / 5f, 3f);

// After this point power is lowered
// This wraps around to the begining of the function
sm.Power = Math.Max(sm.Power - Math.Min(powerReduction, sm.Power * 0.8f) * powerlossInhibitor, 0f);

#endregion
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2026 Goob Station Contributors
//
// SPDX-License-Identifier: MPL-2.0

using Content.Goobstation.Shared.Supermatter.Components;
using Content.Goobstation.Shared.Supermatter.Systems;
using Content.Shared.Atmos;

namespace Content.Goobstation.Server.Supermatter.Systems;

public sealed partial class SupermatterSystem : SharedSupermatterSystem
{
/// <summary>
/// Handles environmental damage.
/// </summary>
private void HandleDamage(EntityUid uid, SupermatterComponent sm)
{
var damageArchived = sm.Damage;

#region Get gas info

var mix = _atmosphere.GetContainingMixture(uid, true, true);

// We're in space or there is no gas to process
if (mix is not { } || mix.TotalMoles == 0f)
{
sm.Damage += Math.Max(sm.Power / 1000 * sm.DamageIncreaseMultiplier, 0.1f);
return;
}

// Absorbed gas from surrounding area
using var surrounding = new GasWrapper(mix, sm.GasEfficiency, _atmosphere);
var moles = surrounding.Gas.TotalMoles;
var (_, _, _, _, heatResistModifier) = surrounding.Gas.GetGasModifiers();

#endregion

var totalDamage = 0f;

var tempThreshold = (Atmospherics.T0C + sm.HeatPenaltyThreshold) * heatResistModifier;

// Temperature start to have a positive effect on damage after 350
var tempDamage = Math.Max(Math.Clamp(moles / 200f, .5f, 1f) * surrounding.Gas.Temperature - tempThreshold, 0f) * sm.MoleHeatThreshold / 150f * sm.DamageIncreaseMultiplier;
totalDamage += tempDamage;

// Power only starts affecting damage when it is above 5000
var powerDamage = Math.Max(sm.Power - sm.PowerPenaltyThreshold, 0f) / 500f * sm.DamageIncreaseMultiplier;
totalDamage += powerDamage;

// Molar count only starts affecting damage when it is above 1800
var moleDamage = Math.Max(moles - sm.MolePenaltyThreshold, 0) / 80 * sm.DamageIncreaseMultiplier;
totalDamage += moleDamage;

// Healing damage
if (moles < sm.MolePenaltyThreshold)
{
// left there a very small float value so that it doesn't eventually divide by 0.
var healHeatDamage = Math.Min(surrounding.Gas.Temperature - tempThreshold, 0.001f) / 150;
totalDamage += healHeatDamage;
}

sm.Damage = Math.Min(damageArchived + sm.DamageHardcap * sm.DelaminationPoint, totalDamage);
sm.DamageDelta = sm.Damage - damageArchived;
}
}
Loading
Loading