using Content.Shared.Administration.Logs; using Content.Shared.Anomaly.Components; using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Weapons.Melee.Events; using Robust.Shared.GameStates; using Robust.Shared.Network; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Shared.Anomaly; public abstract class SharedAnomalySystem : EntitySystem { [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] private readonly INetManager _net = default!; [Dependency] protected readonly IRobustRandom Random = default!; [Dependency] protected readonly ISharedAdminLogManager Log = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!; [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!; private ISawmill _sawmill = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnAnomalyGetState); SubscribeLocalEvent(OnAnomalyHandleState); SubscribeLocalEvent(OnSupercriticalGetState); SubscribeLocalEvent(OnSupercriticalHandleState); SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnAttacked); SubscribeLocalEvent(OnAnomalyUnpause); SubscribeLocalEvent(OnPulsingUnpause); SubscribeLocalEvent(OnSupercriticalUnpause); _sawmill = Logger.GetSawmill("anomaly"); } private void OnAnomalyGetState(EntityUid uid, AnomalyComponent component, ref ComponentGetState args) { args.State = new AnomalyComponentState( component.Severity, component.Stability, component.Health, component.NextPulseTime); } private void OnAnomalyHandleState(EntityUid uid, AnomalyComponent component, ref ComponentHandleState args) { if (args.Current is not AnomalyComponentState state) return; component.Severity = state.Severity; component.Stability = state.Stability; component.Health = state.Health; component.NextPulseTime = state.NextPulseTime; } private void OnSupercriticalGetState(EntityUid uid, AnomalySupercriticalComponent component, ref ComponentGetState args) { args.State = new AnomalySupercriticalComponentState { EndTime = component.EndTime, Duration = component.SupercriticalDuration }; } private void OnSupercriticalHandleState(EntityUid uid, AnomalySupercriticalComponent component, ref ComponentHandleState args) { if (args.Current is not AnomalySupercriticalComponentState state) return; component.EndTime = state.EndTime; component.SupercriticalDuration = state.Duration; } private void OnInteractHand(EntityUid uid, AnomalyComponent component, InteractHandEvent args) { DoAnomalyBurnDamage(uid, args.User, component); args.Handled = true; } private void OnAttacked(EntityUid uid, AnomalyComponent component, AttackedEvent args) { DoAnomalyBurnDamage(uid, args.User, component); } public void DoAnomalyBurnDamage(EntityUid source, EntityUid target, AnomalyComponent component) { _damageable.TryChangeDamage(target, component.AnomalyContactDamage, true); if (!Timing.IsFirstTimePredicted || _net.IsServer) return; Audio.PlayPvs(component.AnomalyContactDamageSound, source); Popup.PopupEntity(Loc.GetString("anomaly-component-contact-damage"), target, target); } private void OnAnomalyUnpause(EntityUid uid, AnomalyComponent component, ref EntityUnpausedEvent args) { component.NextPulseTime += args.PausedTime; Dirty(component); } private void OnPulsingUnpause(EntityUid uid, AnomalyPulsingComponent component, ref EntityUnpausedEvent args) { component.EndTime += args.PausedTime; } private void OnSupercriticalUnpause(EntityUid uid, AnomalySupercriticalComponent component, ref EntityUnpausedEvent args) { component.EndTime += args.PausedTime; Dirty(component); } public void DoAnomalyPulse(EntityUid uid, AnomalyComponent? component = null) { if (!Resolve(uid, ref component)) return; if (!Timing.IsFirstTimePredicted) return; DebugTools.Assert(component.MinPulseLength > TimeSpan.FromSeconds(3)); // this is just to prevent lagspikes mispredicting pulses var variation = Random.NextFloat(-component.PulseVariation, component.PulseVariation) + 1; component.NextPulseTime = Timing.CurTime + GetPulseLength(component) * variation; _sawmill.Info($"Performing anomaly pulse. Entity: {ToPrettyString(uid)}"); // if we are above the growth threshold, then grow before the pulse if (component.Stability > component.GrowthThreshold) { ChangeAnomalySeverity(uid, GetSeverityIncreaseFromGrowth(component), component); } var stability = Random.NextFloat(-component.PulseStabilityVariation, component.PulseStabilityVariation); ChangeAnomalyStability(uid, stability, component); Log.Add(LogType.Anomaly, LogImpact.Medium, $"Anomaly {ToPrettyString(uid)} pulsed with severity {component.Severity}."); if (_net.IsServer) Audio.PlayPvs(component.PulseSound, uid); var pulse = EnsureComp(uid); pulse.EndTime = Timing.CurTime + pulse.PulseDuration; Appearance.SetData(uid, AnomalyVisuals.IsPulsing, true); var ev = new AnomalyPulseEvent(component.Stability, component.Severity); RaiseLocalEvent(uid, ref ev); } /// /// Begins the animation for going supercritical /// /// public void StartSupercriticalEvent(EntityUid uid) { // don't restart it if it's already begun if (HasComp(uid)) return; Log.Add(LogType.Anomaly, LogImpact.High, $"Anomaly {ToPrettyString(uid)} began to go supercritical."); _sawmill.Info($"Anomaly is going supercritical. Entity: {ToPrettyString(uid)}"); var super = EnsureComp(uid); super.EndTime = Timing.CurTime + super.SupercriticalDuration; Appearance.SetData(uid, AnomalyVisuals.Supercritical, true); Dirty(super); } /// /// Does the supercritical event for the anomaly. /// This isn't called once the anomaly reaches the point, but /// after the animation for it going supercritical /// /// /// public void DoAnomalySupercriticalEvent(EntityUid uid, AnomalyComponent? component = null) { if (!Resolve(uid, ref component)) return; if (!Timing.IsFirstTimePredicted) return; Audio.PlayPvs(component.SupercriticalSound, uid); _sawmill.Info($"Raising supercritical event. Entity: {ToPrettyString(uid)}"); var ev = new AnomalySupercriticalEvent(); RaiseLocalEvent(uid, ref ev); EndAnomaly(uid, component, true); } /// /// Ends an anomaly, cleaning up all entities that may be associated with it. /// /// The anomaly being shut down /// /// Whether or not the anomaly ended via supercritical event public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false) { // Logging before resolve, in case the anomaly has deleted itself. _sawmill.Info($"Ending anomaly. Entity: {ToPrettyString(uid)}"); Log.Add(LogType.Anomaly, LogImpact.Extreme, $"Anomaly {ToPrettyString(uid)} went supercritical."); if (!Resolve(uid, ref component)) return; var ev = new AnomalyShutdownEvent(uid, supercritical); RaiseLocalEvent(uid, ref ev, true); if (Terminating(uid) || _net.IsClient) return; Del(uid); } /// /// Changes the stability of the anomaly. /// /// /// /// public void ChangeAnomalyStability(EntityUid uid, float change, AnomalyComponent? component = null) { if (!Resolve(uid, ref component)) return; var newVal = component.Stability + change; component.Stability = Math.Clamp(newVal, 0, 1); Dirty(component); var ev = new AnomalyStabilityChangedEvent(uid, component.Stability); RaiseLocalEvent(uid, ref ev, true); } /// /// Changes the severity of an anomaly, going supercritical if it exceeds 1. /// /// /// /// public void ChangeAnomalySeverity(EntityUid uid, float change, AnomalyComponent? component = null) { if (!Resolve(uid, ref component)) return; var newVal = component.Severity + change; if (newVal >= 1) StartSupercriticalEvent(uid); component.Severity = Math.Clamp(newVal, 0, 1); Dirty(component); var ev = new AnomalySeverityChangedEvent(uid, component.Severity); RaiseLocalEvent(uid, ref ev, true); } /// /// Changes the health of an anomaly, ending it if it's less than 0. /// /// /// /// public void ChangeAnomalyHealth(EntityUid uid, float change, AnomalyComponent? component = null) { if (!Resolve(uid, ref component)) return; var newVal = component.Health + change; if (newVal < 0) { EndAnomaly(uid, component); return; } component.Health = Math.Clamp(newVal, 0, 1); Dirty(component); var ev = new AnomalyHealthChangedEvent(uid, component.Health); RaiseLocalEvent(uid, ref ev, true); } /// /// Gets the length of time between each pulse /// for an anomaly based on its current stability. /// /// /// For anomalies under the instability theshold, this will return the maximum length. /// For those over the theshold, they will return an amount between the maximum and /// minium value based on a linear relationship with the stability. /// /// /// The length of time as a TimeSpan, not including random variation. public TimeSpan GetPulseLength(AnomalyComponent component) { DebugTools.Assert(component.MaxPulseLength > component.MinPulseLength); var modifier = Math.Clamp((component.Stability - component.GrowthThreshold) / component.GrowthThreshold, 0, 1); return (component.MaxPulseLength - component.MinPulseLength) * modifier + component.MinPulseLength; } /// /// Gets the increase in an anomaly's severity due /// to being above its growth threshold /// /// /// The increase in severity for this anomaly private float GetSeverityIncreaseFromGrowth(AnomalyComponent component) { var score = 1 + Math.Max(component.Stability - component.GrowthThreshold, 0) * 10; return score * component.SeverityGrowthCoefficient; } public override void Update(float frameTime) { base.Update(frameTime); var anomalyQuery = EntityQueryEnumerator(); while (anomalyQuery.MoveNext(out var ent, out var anomaly)) { // if the stability is under the death threshold, // update it every second to start killing it slowly. if (anomaly.Stability < anomaly.DecayThreshold) { ChangeAnomalyHealth(ent, anomaly.HealthChangePerSecond * frameTime, anomaly); } if (Timing.CurTime > anomaly.NextPulseTime) { DoAnomalyPulse(ent, anomaly); } } var pulseQuery = EntityQueryEnumerator(); while (pulseQuery.MoveNext(out var ent, out var pulse)) { if (Timing.CurTime > pulse.EndTime) { Appearance.SetData(ent, AnomalyVisuals.IsPulsing, false); RemComp(ent, pulse); } } var supercriticalQuery = EntityQueryEnumerator(); while (supercriticalQuery.MoveNext(out var ent, out var super, out var anom)) { if (Timing.CurTime <= super.EndTime) continue; DoAnomalySupercriticalEvent(ent, anom); RemComp(ent, super); } } }