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 { /// /// 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 (sm.Activated && TryComp(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; } /// /// Shoot lightning bolts depensing on accumulated power. /// 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); } /// /// 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, 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); } /// /// Handles core damage announcements /// 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); } /// If true, sends the message to the common radio /// Localisation string for a custom announcer name 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); } /// /// 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 (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; } /// /// 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; 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(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; } } /// /// Sets the supermatter's status and speech sound based on thresholds /// private void HandleStatus(EntityUid uid, SupermatterComponent sm) { var currentStatus = GetStatus(uid, sm); if (sm.Status != currentStatus) { sm.Status = currentStatus; if (!TryComp(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? 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(uid, out var speech)) return; sm.StatusCurrentSound = null; speech.SpeechSounds = null; } } /// /// 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; 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); } /// /// Plays normal/delam sounds at a rate determined by power and damage /// private void HandleAccent(EntityUid uid, SupermatterComponent sm) { var emit = Comp(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); } }