using Content.Shared.Atmos; using Content.Shared.Radiation.Components; using Content.Shared.Supermatter.Components; using System.Text; using Content.Shared.Chat; using System.Linq; using Content.Shared.Audio; using Content.Shared.CCVar; namespace Content.Server.Supermatter.Systems; public sealed partial class SupermatterSystem { /// /// Handle power and radiation output depending on atmospheric things. /// 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 (TryComp(uid, out var rad)) rad.Intensity = sm.Power * Math.Max(0, 1f + transmissionBonus / 10f) * 0.003f * _config.GetCVar(CCVars.SupermatterRadsModifier); // 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); } /// /// Shoot lightning bolts depensing on accumulated power. /// private void SupermatterZap(EntityUid uid, SupermatterComponent sm) { // Divide power by its' threshold to get a value from 0-1, then multiply by the amount of possible lightnings var zapPower = sm.Power / sm.PowerPenaltyThreshold * sm.LightningPrototypes.Length; var zapPowerNorm = (int) Math.Clamp(zapPower, 0, sm.LightningPrototypes.Length - 1); _lightning.ShootRandomLightnings(uid, 3.5f, sm.Power > sm.PowerPenaltyThreshold ? 3 : 1, sm.LightningPrototypes[zapPowerNorm]); } /// /// Handles environmental damage. /// 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, 0) / 80 * sm.DamageIncreaseMultiplier; totalDamage += moleDamage; // Healing damage if (moles < sm.MolePenaltyThreshold) { // There's a very small float so that it doesn't divide by 0 var healHeatDamage = Math.Min(absorbedGas.Temperature - tempThreshold, 0.001f) / 150; totalDamage += healHeatDamage; } // Return the manipulated gas back to the mix _atmosphere.Merge(mix, absorbedGas); // 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, 0, sm.MaxSpaceExposureDamage); break; } var damage = Math.Min(sm.DamageArchived + sm.DamageHardcap * sm.DamageDelaminationPoint, totalDamage); // Prevent it from going negative sm.Damage = Math.Clamp(damage, 0, float.PositiveInfinity); } /// /// Handles core damage announcements /// private void AnnounceCoreDamage(EntityUid uid, SupermatterComponent sm) { var message = string.Empty; var global = false; var integrity = GetIntegrity(sm).ToString("0.00"); // Special cases if (sm.Damage < sm.DamageDelaminationPoint && sm.Delamming) { message = Loc.GetString("supermatter-delam-cancel", ("integrity", integrity)); sm.DelamAnnounced = false; global = true; } 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; } var station = _station.GetOwningStation(uid); if (station != null) _alert.SetLevel((EntityUid) station, sm.AlertCodeDeltaId, true, true, true, false); sb.AppendLine(Loc.GetString(loc)); sb.AppendLine(Loc.GetString("supermatter-seconds-before-delam", ("seconds", sm.DelamTimer))); message = sb.ToString(); global = true; sm.DelamAnnounced = true; SendSupermatterAnnouncement(uid, 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, message, global); } /// If true, sends a station announcement /// Localisation string for a custom announcer name public void SendSupermatterAnnouncement(EntityUid uid, string message, bool global = false, string? customSender = null) { if (global) { var sender = Loc.GetString(customSender != null ? customSender : "supermatter-announcer"); _chat.DispatchStationAnnouncement(uid, message, sender, colorOverride: Color.Yellow); return; } _chat.TrySendInGameICMessage(uid, message, InGameICChatType.Speak, hideChat: false, checkRadioPrefix: true); } /// /// Returns the integrity rounded to hundreds, e.g. 100.00% /// 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; } /// /// Decide on how to delaminate. /// public DelamType ChooseDelamType(EntityUid uid, SupermatterComponent sm) { if (_config.GetCVar(CCVars.SupermatterDoForceDelam)) return _config.GetCVar(CCVars.SupermatterForcedDelamType); var mix = _atmosphere.GetContainingMixture(uid, true, true); if (mix is { }) { var absorbedGas = mix.Remove(sm.GasEfficiency * mix.TotalMoles); var moles = absorbedGas.TotalMoles; if (_config.GetCVar(CCVars.SupermatterDoSingulooseDelam) && moles >= sm.MolePenaltyThreshold * _config.GetCVar(CCVars.SupermatterSingulooseMolesModifier)) return DelamType.Singulo; } if (_config.GetCVar(CCVars.SupermatterDoTeslooseDelam) && sm.Power >= sm.PowerPenaltyThreshold * _config.GetCVar(CCVars.SupermatterTesloosePowerModifier)) return DelamType.Tesla; //TODO: Add resonance cascade when there's crazy conditions or a destabilizing crystal return DelamType.Explosion; } /// /// Handle the end of the station. /// private void HandleDelamination(EntityUid uid, SupermatterComponent sm) { var xform = Transform(uid); sm.PreferredDelamType = ChooseDelamType(uid, sm); if (!sm.Delamming) { sm.Delamming = true; AnnounceCoreDamage(uid, sm); } if (sm.Damage < sm.DamageDelaminationPoint && sm.Delamming) { sm.Delamming = false; AnnounceCoreDamage(uid, sm); } sm.DelamTimerAccumulator++; if (sm.DelamTimerAccumulator < sm.DelamTimer) return; 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; } } /// /// Swaps out ambience sounds when the SM is delamming or not. /// private void HandleSoundLoop(EntityUid uid, SupermatterComponent sm) { var ambient = Comp(uid); if (ambient == null) return; if (sm.Delamming && sm.CurrentSoundLoop != sm.DelamSound) sm.CurrentSoundLoop = sm.DelamSound; else if (!sm.Delamming && sm.CurrentSoundLoop != sm.CalmSound) sm.CurrentSoundLoop = sm.CalmSound; if (ambient.Sound != sm.CurrentSoundLoop) _ambient.SetSound(uid, sm.CurrentSoundLoop, ambient); } }