using Content.Shared.Administration.Logs; using Content.Shared.Anomaly.Components; using Content.Shared.Anomaly.Prototypes; using Content.Shared.Database; using Content.Shared.Physics; using Content.Shared.Popups; using Content.Shared.Weapons.Melee.Components; using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; using System.Linq; using System.Numerics; using Content.Shared.Actions; 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 AdminLog = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!; [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnAnomalyThrowStart); SubscribeLocalEvent(OnAnomalyThrowEnd); } private void OnAnomalyThrowStart(Entity ent, ref MeleeThrowOnHitStartEvent args) { if (!TryComp(args.Used, out var corePowered) || !TryComp(ent, out var body)) return; _physics.SetBodyType(ent, BodyType.Dynamic, body: body); ChangeAnomalyStability(ent, Random.NextFloat(corePowered.StabilityPerThrow.X, corePowered.StabilityPerThrow.Y), ent.Comp); } private void OnAnomalyThrowEnd(Entity ent, ref MeleeThrowOnHitEndEvent args) { _physics.SetBodyType(ent, BodyType.Static); } 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 RefreshPulseTimer(uid, component); if (_net.IsServer) Log.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 minStability = component.PulseStabilityVariation.X * component.Severity; var maxStability = component.PulseStabilityVariation.Y * component.Severity; var stability = Random.NextFloat(minStability, maxStability); ChangeAnomalyStability(uid, stability, component); AdminLog.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 powerMod = 1f; if (component.CurrentBehavior != null) { var beh = _prototype.Index(component.CurrentBehavior); powerMod = beh.PulsePowerModifier; } var ev = new AnomalyPulseEvent(uid, component.Stability, component.Severity, powerMod); RaiseLocalEvent(uid, ref ev, true); } public void RefreshPulseTimer(EntityUid uid, AnomalyComponent? component = null) { if (!Resolve(uid, ref component)) return; var variation = Random.NextFloat(-component.PulseVariation, component.PulseVariation) + 1; component.NextPulseTime = Timing.CurTime + GetPulseLength(component) * variation; } /// /// Begins the animation for going supercritical /// /// public void StartSupercriticalEvent(EntityUid uid) { // don't restart it if it's already begun if (HasComp(uid)) return; AdminLog.Add(LogType.Anomaly, LogImpact.Extreme, $"Anomaly {ToPrettyString(uid)} began to go supercritical."); if (_net.IsServer) Log.Info($"Anomaly is going supercritical. Entity: {ToPrettyString(uid)}"); var super = AddComp(uid); super.EndTime = Timing.CurTime + super.SupercriticalDuration; Appearance.SetData(uid, AnomalyVisuals.Supercritical, true); Dirty(uid, 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, Transform(uid).Coordinates); if (_net.IsServer) Log.Info($"Raising supercritical event. Entity: {ToPrettyString(uid)}"); var powerMod = 1f; if (component.CurrentBehavior != null) { var beh = _prototype.Index(component.CurrentBehavior); powerMod = beh.PulsePowerModifier; } var ev = new AnomalySupercriticalEvent(uid, powerMod); RaiseLocalEvent(uid, ref ev, true); 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 /// Create anomaly cores based on the result of completing an anomaly? public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true) { // Logging before resolve, in case the anomaly has deleted itself. if (_net.IsServer) Log.Info($"Ending anomaly. Entity: {ToPrettyString(uid)}"); AdminLog.Add(LogType.Anomaly, supercritical ? LogImpact.High : LogImpact.Low, $"Anomaly {ToPrettyString(uid)} {(supercritical ? "went supercritical" : "decayed")}."); if (!Resolve(uid, ref component)) return; var ev = new AnomalyShutdownEvent(uid, supercritical); RaiseLocalEvent(uid, ref ev, true); if (Terminating(uid) || _net.IsClient) return; if (spawnCore) { var core = Spawn(supercritical ? component.CorePrototype : component.CoreInertPrototype, Transform(uid).Coordinates); _transform.PlaceNextTo(core, uid); } if (component.DeleteEntity) QueueDel(uid); else RemCompDeferred(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(uid, component); var ev = new AnomalyStabilityChangedEvent(uid, component.Stability, component.Severity); 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(uid, component); var ev = new AnomalySeverityChangedEvent(uid, component.Stability, 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(uid, 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); var lenght = (component.MaxPulseLength - component.MinPulseLength) * modifier + component.MinPulseLength; //Apply behavior modifier if (component.CurrentBehavior != null) { var behavior = _prototype.Index(component.CurrentBehavior.Value); lenght *= behavior.PulseFrequencyModifier; } return lenght; } /// /// 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); } } /// /// Gets random points around the anomaly based on the given parameters. /// public List? GetSpawningPoints(EntityUid uid, float stability, float severity, AnomalySpawnSettings settings, float powerModifier = 1f) { var xform = Transform(uid); if (!TryComp(xform.GridUid, out var grid)) return null; var amount = (int) (MathHelper.Lerp(settings.MinAmount, settings.MaxAmount, severity * stability * powerModifier) + 0.5f); var localpos = xform.Coordinates.Position; var tilerefs = grid.GetLocalTilesIntersecting( new Box2(localpos + new Vector2(-settings.MaxRange, -settings.MaxRange), localpos + new Vector2(settings.MaxRange, settings.MaxRange))).ToList(); if (tilerefs.Count == 0) return null; var physQuery = GetEntityQuery(); var resultList = new List(); while (resultList.Count < amount) { if (tilerefs.Count == 0) break; var tileref = Random.Pick(tilerefs); var distance = MathF.Sqrt(MathF.Pow(tileref.X - xform.LocalPosition.X, 2) + MathF.Pow(tileref.Y - xform.LocalPosition.Y, 2)); //cut outer & inner circle if (distance > settings.MaxRange || distance < settings.MinRange) { tilerefs.Remove(tileref); continue; } if (!settings.CanSpawnOnEntities) { var valid = true; foreach (var ent in grid.GetAnchoredEntities(tileref.GridIndices)) { if (!physQuery.TryGetComponent(ent, out var body)) continue; if (body.BodyType != BodyType.Static || !body.Hard || (body.CollisionLayer & (int) CollisionGroup.Impassable) == 0) continue; valid = false; break; } if (!valid) { tilerefs.Remove(tileref); continue; } } resultList.Add(tileref); } return resultList; } } [DataRecord] public partial record struct AnomalySpawnSettings() { /// /// should entities block spawning? /// public bool CanSpawnOnEntities { get; set; } = false; /// /// The minimum number of entities that spawn per pulse /// public int MinAmount { get; set; } = 0; /// /// The maximum number of entities that spawn per pulse /// scales with severity. /// public int MaxAmount { get; set; } = 1; /// /// The distance from the anomaly in which the entities will not appear /// public float MinRange { get; set; } = 0f; /// /// The maximum radius the entities will spawn in. /// public float MaxRange { get; set; } = 1f; /// /// Whether or not anomaly spawns entities on Pulse /// public bool SpawnOnPulse { get; set; } = false; /// /// Whether or not anomaly spawns entities on SuperCritical /// public bool SpawnOnSuperCritical { get; set; } = false; /// /// Whether or not anomaly spawns entities when destroyed /// public bool SpawnOnShutdown { get; set; } = false; /// /// Whether or not anomaly spawns entities on StabilityChanged /// public bool SpawnOnStabilityChanged { get; set; } = false; /// /// Whether or not anomaly spawns entities on SeverityChanged /// public bool SpawnOnSeverityChanged { get; set; } = false; } public sealed partial class ActionAnomalyPulseEvent : InstantActionEvent { }