| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480 |
- 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.Throwing;
- 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!;
- [Dependency] private readonly SharedMapSystem _map = default!;
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent<AnomalyComponent, MeleeThrowOnHitStartEvent>(OnAnomalyThrowStart);
- SubscribeLocalEvent<AnomalyComponent, LandEvent>(OnLand);
- }
- private void OnAnomalyThrowStart(Entity<AnomalyComponent> ent, ref MeleeThrowOnHitStartEvent args)
- {
- if (!TryComp<CorePoweredThrowerComponent>(args.Weapon, out var corePowered) || !TryComp<PhysicsComponent>(ent, out var body))
- return;
- // anomalies are static by default, so we have set them to dynamic to be throwable
- _physics.SetBodyType(ent, BodyType.Dynamic, body: body);
- ChangeAnomalyStability(ent, Random.NextFloat(corePowered.StabilityPerThrow.X, corePowered.StabilityPerThrow.Y), ent.Comp);
- }
- private void OnLand(Entity<AnomalyComponent> ent, ref LandEvent args)
- {
- // revert back to static
- _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<AnomalyPulsingComponent>(uid);
- pulse.EndTime = Timing.CurTime + pulse.PulseDuration;
- Appearance.SetData(uid, AnomalyVisuals.IsPulsing, true);
- var powerMod = 1f;
- if (component.CurrentBehavior != null)
- {
- var beh = _prototype.Index<AnomalyBehaviorPrototype>(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;
- }
- /// <summary>
- /// Begins the animation for going supercritical
- /// </summary>
- /// <param name="uid"></param>
- public void StartSupercriticalEvent(EntityUid uid)
- {
- // don't restart it if it's already begun
- if (HasComp<AnomalySupercriticalComponent>(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<AnomalySupercriticalComponent>(uid);
- super.EndTime = Timing.CurTime + super.SupercriticalDuration;
- Appearance.SetData(uid, AnomalyVisuals.Supercritical, true);
- Dirty(uid, super);
- }
- /// <summary>
- /// 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
- /// </summary>
- /// <param name="uid"></param>
- /// <param name="component"></param>
- 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<AnomalyBehaviorPrototype>(component.CurrentBehavior);
- powerMod = beh.PulsePowerModifier;
- }
- var ev = new AnomalySupercriticalEvent(uid, powerMod);
- RaiseLocalEvent(uid, ref ev, true);
- EndAnomaly(uid, component, true, logged: true);
- }
- /// <summary>
- /// Ends an anomaly, cleaning up all entities that may be associated with it.
- /// </summary>
- /// <param name="uid">The anomaly being shut down</param>
- /// <param name="component"></param>
- /// <param name="supercritical">Whether or not the anomaly ended via supercritical event</param>
- /// <param name="spawnCore">Create anomaly cores based on the result of completing an anomaly?</param>
- /// <param name="logged">Whether or not the anomaly decaying/going supercritical is logged</param>
- public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true, bool logged = false)
- {
- if (logged)
- {
- // 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<AnomalySupercriticalComponent>(uid);
- }
- /// <summary>
- /// Changes the stability of the anomaly.
- /// </summary>
- /// <param name="uid"></param>
- /// <param name="change"></param>
- /// <param name="component"></param>
- 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);
- }
- /// <summary>
- /// Changes the severity of an anomaly, going supercritical if it exceeds 1.
- /// </summary>
- /// <param name="uid"></param>
- /// <param name="change"></param>
- /// <param name="component"></param>
- 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);
- }
- /// <summary>
- /// Changes the health of an anomaly, ending it if it's less than 0.
- /// </summary>
- /// <param name="uid"></param>
- /// <param name="change"></param>
- /// <param name="component"></param>
- 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, logged: true);
- return;
- }
- component.Health = Math.Clamp(newVal, 0, 1);
- Dirty(uid, component);
- var ev = new AnomalyHealthChangedEvent(uid, component.Health);
- RaiseLocalEvent(uid, ref ev, true);
- }
- /// <summary>
- /// Gets the length of time between each pulse
- /// for an anomaly based on its current stability.
- /// </summary>
- /// <remarks>
- /// 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.
- /// </remarks>
- /// <param name="component"></param>
- /// <returns>The length of time as a TimeSpan, not including random variation.</returns>
- 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;
- }
- /// <summary>
- /// Gets the increase in an anomaly's severity due
- /// to being above its growth threshold
- /// </summary>
- /// <param name="component"></param>
- /// <returns>The increase in severity for this anomaly</returns>
- 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<AnomalyComponent>();
- 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<AnomalyPulsingComponent>();
- 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<AnomalySupercriticalComponent, AnomalyComponent>();
- while (supercriticalQuery.MoveNext(out var ent, out var super, out var anom))
- {
- if (Timing.CurTime <= super.EndTime)
- continue;
- DoAnomalySupercriticalEvent(ent, anom);
- RemComp(ent, super);
- }
- }
- /// <summary>
- /// Gets random points around the anomaly based on the given parameters.
- /// </summary>
- public List<TileRef>? GetSpawningPoints(EntityUid uid, float stability, float severity, AnomalySpawnSettings settings, float powerModifier = 1f)
- {
- var xform = Transform(uid);
- if (!TryComp<MapGridComponent>(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 = _map.GetLocalTilesIntersecting(
- xform.GridUid.Value,
- grid,
- 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<PhysicsComponent>();
- var resultList = new List<TileRef>();
- 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()
- {
- /// <summary>
- /// should entities block spawning?
- /// </summary>
- public bool CanSpawnOnEntities { get; set; } = false;
- /// <summary>
- /// The minimum number of entities that spawn per pulse
- /// </summary>
- public int MinAmount { get; set; } = 0;
- /// <summary>
- /// The maximum number of entities that spawn per pulse
- /// scales with severity.
- /// </summary>
- public int MaxAmount { get; set; } = 1;
- /// <summary>
- /// The distance from the anomaly in which the entities will not appear
- /// </summary>
- public float MinRange { get; set; } = 0f;
- /// <summary>
- /// The maximum radius the entities will spawn in.
- /// </summary>
- public float MaxRange { get; set; } = 1f;
- /// <summary>
- /// Whether or not anomaly spawns entities on Pulse
- /// </summary>
- public bool SpawnOnPulse { get; set; } = false;
- /// <summary>
- /// Whether or not anomaly spawns entities on SuperCritical
- /// </summary>
- public bool SpawnOnSuperCritical { get; set; } = false;
- /// <summary>
- /// Whether or not anomaly spawns entities when destroyed
- /// </summary>
- public bool SpawnOnShutdown { get; set; } = false;
- /// <summary>
- /// Whether or not anomaly spawns entities on StabilityChanged
- /// </summary>
- public bool SpawnOnStabilityChanged { get; set; } = false;
- /// <summary>
- /// Whether or not anomaly spawns entities on SeverityChanged
- /// </summary>
- public bool SpawnOnSeverityChanged { get; set; } = false;
- }
- public sealed partial class ActionAnomalyPulseEvent : InstantActionEvent { }
|