Files
wwdpublic/Content.Server/_EE/Supermatter/Systems/SupermatterSystem.Processing.cs
VMSolidus db610d403b Supermatter Tweaks (#2423)
# Description

_config.GetCVar() is generally to be deprecated and replaced with the
significantly better performant Subs.CVar(), since it doesn't need to
fetch the CVar each time its used and instead keeps it cached. This is
particularly important for systems that operate on every frame.
Technically Supermatter wasn't on my list of systems to optimize, but
it's a good look since it's a system we're uniquely responsible for.

No actual functionality for the Supermatter has been changed, this is
just a code cleanup and performance pass.

# Changelog

No CL since this isn't player facing.

(cherry picked from commit 1d10885f3a24053f77f8a27736e4c198b2aac026)
2025-05-16 20:06:44 +03:00

615 lines
22 KiB
C#

using System.Linq;
using System.Text;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Sound.Components;
using Content.Shared._EE.Supermatter.Components;
using Content.Shared._EE.Supermatter.Monitor;
using Content.Shared.Atmos;
using Content.Shared.Audio;
using Content.Shared.Chat;
using Content.Shared.Popups;
using Content.Shared.Radiation.Components;
using Content.Shared.Speech;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server._EE.Supermatter.Systems;
public sealed partial class SupermatterSystem
{
/// <summary>
/// Handle power and radiation output depending on atmospheric things.
/// </summary>
private void ProcessAtmos(EntityUid uid, SupermatterComponent sm, float frameTime)
{
var mix = _atmosphere.GetContainingMixture(uid, true, true);
if (mix is not { })
return;
var absorbedGas = mix.Remove(sm.GasEfficiency * mix.TotalMoles);
var moles = absorbedGas.TotalMoles;
if (!(moles > 0f))
return;
var gases = sm.GasStorage;
var facts = sm.GasDataFields;
// Lets get the proportions of the gasses in the mix for scaling stuff later
// They range between 0 and 1
gases = gases.ToDictionary(
gas => gas.Key,
gas => Math.Clamp(absorbedGas.GetMoles(gas.Key) / moles, 0, 1)
);
// No less then zero, and no greater then one, we use this to do explosions and heat to power transfer.
var powerRatio = gases.Sum(gas => gases[gas.Key] * facts[gas.Key].PowerMixRatio);
// Minimum value of -10, maximum value of 23. Affects plasma, o2 and heat output.
var heatModifier = gases.Sum(gas => gases[gas.Key] * facts[gas.Key].HeatPenalty);
// Minimum value of -10, maximum value of 23. Affects plasma, o2 and heat output.
var transmissionBonus = gases.Sum(gas => gases[gas.Key] * facts[gas.Key].TransmitModifier);
var h2OBonus = 1 - gases[Gas.WaterVapor] * 0.25f;
powerRatio = Math.Clamp(powerRatio, 0, 1);
heatModifier = Math.Max(heatModifier, 0.5f);
transmissionBonus *= h2OBonus;
// Effects the damage heat does to the crystal
sm.DynamicHeatResistance = 1f;
// More moles of gases are harder to heat than fewer, so let's scale heat damage around them
sm.MoleHeatPenaltyThreshold = (float) Math.Max(moles * sm.MoleHeatPenalty, 0.25);
// Ramps up or down in increments of 0.02 up to the proportion of CO2
// Given infinite time, powerloss_dynamic_scaling = co2comp
// Some value from 0-1
if (moles > sm.PowerlossInhibitionMoleThreshold && gases[Gas.CarbonDioxide] > sm.PowerlossInhibitionGasThreshold)
{
var co2powerloss = Math.Clamp(gases[Gas.CarbonDioxide] - sm.PowerlossDynamicScaling, -0.02f, 0.02f);
sm.PowerlossDynamicScaling = Math.Clamp(sm.PowerlossDynamicScaling + co2powerloss, 0f, 1f);
}
else
sm.PowerlossDynamicScaling = Math.Clamp(sm.PowerlossDynamicScaling - 0.05f, 0f, 1f);
// Ranges from 0~1(1 - (0~1 * 1~(1.5 * (mol / 500))))
// We take the mol count, and scale it to be our inhibitor
var powerlossInhibitor =
Math.Clamp(
1
- sm.PowerlossDynamicScaling
* Math.Clamp(
moles / sm.PowerlossInhibitionMoleBoostThreshold,
1f, 1.5f),
0f, 1f);
if (sm.MatterPower != 0) // We base our removed power off 1/10 the matter_power.
{
var removedMatter = Math.Max(sm.MatterPower / sm.MatterPowerConversion, 40);
// Adds at least 40 power
sm.Power = Math.Max(sm.Power + removedMatter, 0);
// Removes at least 40 matter power
sm.MatterPower = Math.Max(sm.MatterPower - removedMatter, 0);
}
// Based on gas mix, makes the power more based on heat or less effected by heat
var tempFactor = powerRatio > 0.8 ? 50f : 30f;
// If there is more pluox and N2 then anything else, we receive no power increase from heat
sm.Power = Math.Max(absorbedGas.Temperature * tempFactor / Atmospherics.T0C * powerRatio + sm.Power, 0);
// Irradiate stuff
if (sm.Activated && TryComp<RadiationSourceComponent>(uid, out var rad))
{
rad.Intensity =
SupermatterRadsBase +
(sm.Power
* Math.Max(0, 1f + transmissionBonus / 10f)
* 0.003f
* SupermatterRadsModifier);
rad.Slope = Math.Clamp(rad.Intensity / 15, 0.2f, 1f);
}
// Power * 0.55 * 0.8~1
// This has to be differentiated with respect to time, since its going to be interacting with systems
// that also differentiate. Basically, if we don't multiply by 2 * frameTime, the supermatter will explode faster if your server's tickrate is higher.
var energy = 2 * sm.Power * sm.ReactionPowerModifier * frameTime;
// Keep in mind we are only adding this temperature to (efficiency)% of the one tile the rock is on.
// An increase of 4°C at 25% efficiency here results in an increase of 1°C / (#tilesincore) overall.
// Power * 0.55 * 1.5~23 / 5
absorbedGas.Temperature += energy * heatModifier * sm.ThermalReleaseModifier;
absorbedGas.Temperature = Math.Max(0,
Math.Min(absorbedGas.Temperature, sm.HeatThreshold * heatModifier));
// Release the waste
absorbedGas.AdjustMoles(Gas.Plasma, Math.Max(energy * heatModifier * sm.PlasmaReleaseModifier, 0f));
absorbedGas.AdjustMoles(Gas.Oxygen, Math.Max((energy + absorbedGas.Temperature * heatModifier - Atmospherics.T0C) * sm.OxygenReleaseEfficiencyModifier, 0f));
_atmosphere.Merge(mix, absorbedGas);
var powerReduction = (float) Math.Pow(sm.Power / 500, 3);
// After this point power is lowered
// This wraps around to the begining of the function
sm.Power = Math.Max(sm.Power - Math.Min(powerReduction * powerlossInhibitor, sm.Power * 0.83f * powerlossInhibitor), 0f);
// Save values to the supermatter
sm.GasStorage = sm.GasStorage.ToDictionary(
gas => gas.Key,
gas => absorbedGas.GetMoles(gas.Key)
);
sm.Temperature = absorbedGas.Temperature;
sm.WasteMultiplier = heatModifier;
}
/// <summary>
/// Shoot lightning bolts depensing on accumulated power.
/// </summary>
private void SupermatterZap(EntityUid uid, SupermatterComponent sm)
{
var zapPower = 0;
var zapCount = 0;
var zapRange = Math.Clamp(sm.Power / 1000, 2, 7);
// fuck this
if (_random.Prob(0.05f))
{
zapCount += 1;
}
if (sm.Power >= sm.PowerPenaltyThreshold)
{
zapCount += 2;
}
if (sm.Power >= sm.SeverePowerPenaltyThreshold)
{
zapPower = 1;
zapCount++;
}
if (sm.Power >= sm.CriticalPowerPenaltyThreshold)
{
zapPower = 2;
zapCount++;
}
if (zapCount >= 1)
_lightning.ShootRandomLightnings(uid, zapRange, zapCount, sm.LightningPrototypes[zapPower], hitCoordsChance: sm.ZapHitCoordinatesChance);
}
/// <summary>
/// Handles environmental damage.
/// </summary>
private void HandleDamage(EntityUid uid, SupermatterComponent sm)
{
var xform = Transform(uid);
var indices = _xform.GetGridOrMapTilePosition(uid, xform);
sm.DamageArchived = sm.Damage;
var mix = _atmosphere.GetContainingMixture(uid, true, true);
// We're in space or there is no gas to process
if (!xform.GridUid.HasValue || mix is not { } || mix.TotalMoles == 0f)
{
sm.Damage += Math.Max(sm.Power / 1000 * sm.DamageIncreaseMultiplier, 0.1f);
return;
}
// Absorbed gas from surrounding area
var absorbedGas = mix.Remove(sm.GasEfficiency * mix.TotalMoles);
var moles = absorbedGas.TotalMoles;
var totalDamage = 0f;
var tempThreshold = Atmospherics.T0C + sm.HeatPenaltyThreshold;
// Temperature start to have a positive effect on damage after 350
var tempDamage =
Math.Max(
Math.Clamp(moles / 200f, .5f, 1f)
* absorbedGas.Temperature
- tempThreshold
* sm.DynamicHeatResistance,
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;
// Mol count only starts affecting damage when it is above 1800
var moleDamage = Math.Max(moles - sm.MolePenaltyThreshold, 0f) / 80 * sm.DamageIncreaseMultiplier;
totalDamage += moleDamage;
// Healing damage
if (moles < sm.MolePenaltyThreshold)
{
var healHeatDamage = Math.Min(absorbedGas.Temperature - tempThreshold, 0f) / 150;
totalDamage += healHeatDamage;
}
// Check for space tiles next to SM
//TODO: Change moles out for checking if adjacent tiles exist
var enumerator = _atmosphere.GetAdjacentTileMixtures(xform.GridUid.Value, indices, false, false);
while (enumerator.MoveNext(out var ind))
{
if (ind.TotalMoles != 0)
continue;
var integrity = GetIntegrity(sm);
var factor = integrity switch
{
< 10 => 0.0005f,
< 25 => 0.0009f,
< 45 => 0.005f,
< 75 => 0.002f,
_ => 0f
};
totalDamage += Math.Clamp(sm.Power * factor * sm.DamageIncreaseMultiplier, 0f, sm.MaxSpaceExposureDamage);
break;
}
var damage = Math.Min(sm.DamageArchived + sm.DamageHardcap * sm.DamageDelaminationPoint, sm.Damage + totalDamage);
// Prevent it from going negative
sm.Damage = Math.Clamp(damage, 0, float.PositiveInfinity);
}
/// <summary>
/// Handles core damage announcements
/// </summary>
private void AnnounceCoreDamage(EntityUid uid, SupermatterComponent sm)
{
// If undamaged, no need to announce anything
if (sm.Damage == 0)
return;
var message = string.Empty;
var global = false;
var integrity = GetIntegrity(sm).ToString("0.00");
// Instantly announce delamination
if (sm.Delamming && !sm.DelamAnnounced)
{
var sb = new StringBuilder();
var loc = string.Empty;
switch (sm.PreferredDelamType)
{
case DelamType.Cascade: loc = "supermatter-delam-cascade"; break;
case DelamType.Singulo: loc = "supermatter-delam-overmass"; break;
case DelamType.Tesla: loc = "supermatter-delam-tesla"; break;
default: loc = "supermatter-delam-explosion"; break;
}
sb.AppendLine(Loc.GetString(loc));
sb.Append(Loc.GetString("supermatter-seconds-before-delam", ("seconds", sm.DelamTimer)));
message = sb.ToString();
global = true;
sm.DelamAnnounced = true;
sm.YellTimer = TimeSpan.FromSeconds(sm.DelamTimer / 2);
SendSupermatterAnnouncement(uid, sm, message, global);
return;
}
// Only announce every YellTimer seconds
if (_timing.CurTime < sm.YellLast + sm.YellTimer)
return;
// Recovered after the delamination point
if (sm.Damage < sm.DamageDelaminationPoint && sm.DelamAnnounced)
{
message = Loc.GetString("supermatter-delam-cancel", ("integrity", integrity));
sm.DelamAnnounced = false;
sm.YellTimer = TimeSpan.FromSeconds(SupermatterYellTimer);
global = true;
SendSupermatterAnnouncement(uid, sm, message, global);
return;
}
// Oh god oh fuck
if (sm.Delamming && sm.DelamAnnounced)
{
var seconds = Math.Ceiling(sm.DelamEndTime.TotalSeconds - _timing.CurTime.TotalSeconds);
if (seconds <= 0)
return;
var loc = seconds switch
{
> 5 => "supermatter-seconds-before-delam-countdown",
<= 5 => "supermatter-seconds-before-delam-imminent",
_ => String.Empty
};
sm.YellTimer = seconds switch
{
> 30 => TimeSpan.FromSeconds(10),
> 5 => TimeSpan.FromSeconds(5),
<= 5 => TimeSpan.FromSeconds(1),
_ => TimeSpan.FromSeconds(SupermatterYellTimer)
};
message = Loc.GetString(loc, ("seconds", seconds));
global = true;
SendSupermatterAnnouncement(uid, sm, message, global);
return;
}
// We're safe
if (sm.Damage < sm.DamageArchived && sm.Status >= SupermatterStatusType.Warning)
{
message = Loc.GetString("supermatter-healing", ("integrity", integrity));
if (sm.Status >= SupermatterStatusType.Emergency)
global = true;
SendSupermatterAnnouncement(uid, sm, message, global);
return;
}
// Ignore the 0% integrity alarm
if (sm.Delamming)
return;
// We are not taking consistent damage, Engineers aren't needed
if (sm.Damage <= sm.DamageArchived)
return;
if (sm.Damage >= sm.DamageWarningThreshold)
{
message = Loc.GetString("supermatter-warning", ("integrity", integrity));
if (sm.Damage >= sm.DamageEmergencyThreshold)
{
message = Loc.GetString("supermatter-emergency", ("integrity", integrity));
global = true;
}
}
SendSupermatterAnnouncement(uid, sm, message, global);
}
/// <param name="global">If true, sends the message to the common radio</param>
/// <param name="customSender">Localisation string for a custom announcer name</param>
public void SendSupermatterAnnouncement(EntityUid uid, SupermatterComponent sm, string message, bool global = false)
{
if (message == String.Empty)
return;
var channel = sm.Channel;
if (global)
channel = sm.ChannelGlobal;
// Ensure status, otherwise the wrong speech sound may be used
HandleStatus(uid, sm);
sm.YellLast = _timing.CurTime;
_chat.TrySendInGameICMessage(uid, message, InGameICChatType.Speak, hideChat: false, checkRadioPrefix: true);
_radio.SendRadioMessage(uid, message, channel, uid);
}
/// <summary>
/// Returns the integrity rounded to hundreds, e.g. 100.00%
/// </summary>
public float GetIntegrity(SupermatterComponent sm)
{
var integrity = sm.Damage / sm.DamageDelaminationPoint;
integrity = (float) Math.Round(100 - integrity * 100, 2);
integrity = integrity < 0 ? 0 : integrity;
return integrity;
}
/// <summary>
/// Decide on how to delaminate.
/// </summary>
public DelamType ChooseDelamType(EntityUid uid, SupermatterComponent sm)
{
if (SupermatterDoForceDelam)
return SupermatterForcedDelamType;
var mix = _atmosphere.GetContainingMixture(uid, true, true);
if (mix is { })
{
var absorbedGas = mix.Remove(sm.GasEfficiency * mix.TotalMoles);
var moles = absorbedGas.TotalMoles;
if (SupermatterDoSingulooseDelam
&& moles >= sm.MolePenaltyThreshold * SupermatterSingulooseMolesModifier)
return DelamType.Singulo;
}
if (SupermatterDoTeslooseDelam
&& sm.Power >= sm.PowerPenaltyThreshold * SupermatterTesloosePowerModifier)
return DelamType.Tesla;
//TODO: Add resonance cascade when there's crazy conditions or a destabilizing crystal
return DelamType.Explosion;
}
/// <summary>
/// Handle the end of the station.
/// </summary>
private void HandleDelamination(EntityUid uid, SupermatterComponent sm)
{
var xform = Transform(uid);
sm.PreferredDelamType = ChooseDelamType(uid, sm);
if (!sm.Delamming)
{
sm.Delamming = true;
sm.DelamEndTime = _timing.CurTime + TimeSpan.FromSeconds(sm.DelamTimer);
AnnounceCoreDamage(uid, sm);
}
if (sm.Damage < sm.DamageDelaminationPoint && sm.Delamming)
{
sm.Delamming = false;
AnnounceCoreDamage(uid, sm);
}
if (_timing.CurTime < sm.DelamEndTime)
return;
var smTransform = Transform(uid);
foreach (var pSession in Filter.GetAllPlayers())
{
var pEntity = pSession.AttachedEntity;
if (pEntity != null
&& TryComp<TransformComponent>(pEntity, out var pTransform)
&& pTransform.MapID == smTransform.MapID)
_popup.PopupEntity(Loc.GetString("supermatter-delam-player"), pEntity.Value, pEntity.Value, PopupType.MediumCaution);
}
_audio.PlayGlobal(sm.DistortSound, Filter.BroadcastMap(Transform(uid).MapID), true);
switch (sm.PreferredDelamType)
{
case DelamType.Cascade:
Spawn(sm.KudzuSpawnPrototype, xform.Coordinates);
break;
case DelamType.Singulo:
Spawn(sm.SingularitySpawnPrototype, xform.Coordinates);
break;
case DelamType.Tesla:
Spawn(sm.TeslaSpawnPrototype, xform.Coordinates);
break;
default:
_explosion.TriggerExplosive(uid);
break;
}
}
/// <summary>
/// Sets the supermatter's status and speech sound based on thresholds
/// </summary>
private void HandleStatus(EntityUid uid, SupermatterComponent sm)
{
var currentStatus = GetStatus(uid, sm);
if (sm.Status != currentStatus)
{
sm.Status = currentStatus;
if (!TryComp<SpeechComponent>(uid, out var speech))
return;
sm.StatusCurrentSound = currentStatus switch
{
SupermatterStatusType.Warning => sm.StatusWarningSound,
SupermatterStatusType.Danger => sm.StatusDangerSound,
SupermatterStatusType.Emergency => sm.StatusEmergencySound,
SupermatterStatusType.Delaminating => sm.StatusDelamSound,
_ => null
};
ProtoId<SpeechSoundsPrototype>? speechSound = sm.StatusCurrentSound;
if (currentStatus == SupermatterStatusType.Warning)
speech.AudioParams = AudioParams.Default.AddVolume(7.5f);
else
speech.AudioParams = AudioParams.Default.AddVolume(10f);
if (currentStatus == SupermatterStatusType.Delaminating)
speech.SoundCooldownTime = 6.8f; // approximate length of bloblarm.ogg
else
speech.SoundCooldownTime = 0.0f;
speech.SpeechSounds = speechSound;
}
// Supermatter is healing, don't play any speech sounds
if (sm.Damage < sm.DamageArchived)
{
if (!TryComp<SpeechComponent>(uid, out var speech))
return;
sm.StatusCurrentSound = null;
speech.SpeechSounds = null;
}
}
/// <summary>
/// Swaps out ambience sounds when the SM is delamming or not.
/// </summary>
private void HandleSoundLoop(EntityUid uid, SupermatterComponent sm)
{
var ambient = Comp<AmbientSoundComponent>(uid);
if (ambient == null)
return;
var volume = (float) Math.Round(Math.Clamp((sm.Power / 50) - 5, -5, 5));
_ambient.SetVolume(uid, volume);
if (sm.Status >= SupermatterStatusType.Danger && sm.CurrentSoundLoop != sm.DelamLoopSound)
sm.CurrentSoundLoop = sm.DelamLoopSound;
else if (sm.Status < SupermatterStatusType.Danger && sm.CurrentSoundLoop != sm.CalmLoopSound)
sm.CurrentSoundLoop = sm.CalmLoopSound;
if (ambient.Sound != sm.CurrentSoundLoop)
_ambient.SetSound(uid, sm.CurrentSoundLoop, ambient);
}
/// <summary>
/// Plays normal/delam sounds at a rate determined by power and damage
/// </summary>
private void HandleAccent(EntityUid uid, SupermatterComponent sm)
{
var emit = Comp<EmitSoundOnTriggerComponent>(uid);
if (emit == null)
return;
if (sm.AccentLastTime >= _timing.CurTime || !_random.Prob(0.05f))
return;
var aggression = Math.Min((sm.Damage / 800) * (sm.Power / 2500), 1) * 100;
var nextSound = Math.Max(Math.Round((100 - aggression) * 5), sm.AccentMinCooldown);
if (sm.AccentLastTime + TimeSpan.FromSeconds(nextSound) > _timing.CurTime)
return;
if (sm.Status >= SupermatterStatusType.Danger && emit.Sound != sm.DelamAccent)
emit.Sound = sm.DelamAccent;
else if (sm.Status < SupermatterStatusType.Danger && emit.Sound != sm.CalmAccent)
emit.Sound = sm.CalmAccent;
sm.AccentLastTime = _timing.CurTime;
var ev = new TriggerEvent(uid);
RaiseLocalEvent(uid, ev);
}
}