1
0

SharedAnomalySystem.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. using Content.Shared.Administration.Logs;
  2. using Content.Shared.Anomaly.Components;
  3. using Content.Shared.Anomaly.Prototypes;
  4. using Content.Shared.Database;
  5. using Content.Shared.Physics;
  6. using Content.Shared.Popups;
  7. using Content.Shared.Throwing;
  8. using Content.Shared.Weapons.Melee.Components;
  9. using Robust.Shared.Audio.Systems;
  10. using Robust.Shared.Map;
  11. using Robust.Shared.Map.Components;
  12. using Robust.Shared.Network;
  13. using Robust.Shared.Physics;
  14. using Robust.Shared.Physics.Components;
  15. using Robust.Shared.Physics.Systems;
  16. using Robust.Shared.Prototypes;
  17. using Robust.Shared.Random;
  18. using Robust.Shared.Timing;
  19. using Robust.Shared.Utility;
  20. using System.Linq;
  21. using System.Numerics;
  22. using Content.Shared.Actions;
  23. namespace Content.Shared.Anomaly;
  24. public abstract class SharedAnomalySystem : EntitySystem
  25. {
  26. [Dependency] protected readonly IGameTiming Timing = default!;
  27. [Dependency] private readonly INetManager _net = default!;
  28. [Dependency] protected readonly IRobustRandom Random = default!;
  29. [Dependency] protected readonly ISharedAdminLogManager AdminLog = default!;
  30. [Dependency] protected readonly SharedAudioSystem Audio = default!;
  31. [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
  32. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  33. [Dependency] protected readonly SharedPopupSystem Popup = default!;
  34. [Dependency] private readonly IPrototypeManager _prototype = default!;
  35. [Dependency] private readonly SharedTransformSystem _transform = default!;
  36. [Dependency] private readonly SharedMapSystem _map = default!;
  37. public override void Initialize()
  38. {
  39. base.Initialize();
  40. SubscribeLocalEvent<AnomalyComponent, MeleeThrowOnHitStartEvent>(OnAnomalyThrowStart);
  41. SubscribeLocalEvent<AnomalyComponent, LandEvent>(OnLand);
  42. }
  43. private void OnAnomalyThrowStart(Entity<AnomalyComponent> ent, ref MeleeThrowOnHitStartEvent args)
  44. {
  45. if (!TryComp<CorePoweredThrowerComponent>(args.Weapon, out var corePowered) || !TryComp<PhysicsComponent>(ent, out var body))
  46. return;
  47. // anomalies are static by default, so we have set them to dynamic to be throwable
  48. _physics.SetBodyType(ent, BodyType.Dynamic, body: body);
  49. ChangeAnomalyStability(ent, Random.NextFloat(corePowered.StabilityPerThrow.X, corePowered.StabilityPerThrow.Y), ent.Comp);
  50. }
  51. private void OnLand(Entity<AnomalyComponent> ent, ref LandEvent args)
  52. {
  53. // revert back to static
  54. _physics.SetBodyType(ent, BodyType.Static);
  55. }
  56. public void DoAnomalyPulse(EntityUid uid, AnomalyComponent? component = null)
  57. {
  58. if (!Resolve(uid, ref component))
  59. return;
  60. if (!Timing.IsFirstTimePredicted)
  61. return;
  62. DebugTools.Assert(component.MinPulseLength > TimeSpan.FromSeconds(3)); // this is just to prevent lagspikes mispredicting pulses
  63. RefreshPulseTimer(uid, component);
  64. if (_net.IsServer)
  65. Log.Info($"Performing anomaly pulse. Entity: {ToPrettyString(uid)}");
  66. // if we are above the growth threshold, then grow before the pulse
  67. if (component.Stability > component.GrowthThreshold)
  68. {
  69. ChangeAnomalySeverity(uid, GetSeverityIncreaseFromGrowth(component), component);
  70. }
  71. var minStability = component.PulseStabilityVariation.X * component.Severity;
  72. var maxStability = component.PulseStabilityVariation.Y * component.Severity;
  73. var stability = Random.NextFloat(minStability, maxStability);
  74. ChangeAnomalyStability(uid, stability, component);
  75. AdminLog.Add(LogType.Anomaly, LogImpact.Medium, $"Anomaly {ToPrettyString(uid)} pulsed with severity {component.Severity}.");
  76. if (_net.IsServer)
  77. Audio.PlayPvs(component.PulseSound, uid);
  78. var pulse = EnsureComp<AnomalyPulsingComponent>(uid);
  79. pulse.EndTime = Timing.CurTime + pulse.PulseDuration;
  80. Appearance.SetData(uid, AnomalyVisuals.IsPulsing, true);
  81. var powerMod = 1f;
  82. if (component.CurrentBehavior != null)
  83. {
  84. var beh = _prototype.Index<AnomalyBehaviorPrototype>(component.CurrentBehavior);
  85. powerMod = beh.PulsePowerModifier;
  86. }
  87. var ev = new AnomalyPulseEvent(uid, component.Stability, component.Severity, powerMod);
  88. RaiseLocalEvent(uid, ref ev, true);
  89. }
  90. public void RefreshPulseTimer(EntityUid uid, AnomalyComponent? component = null)
  91. {
  92. if (!Resolve(uid, ref component))
  93. return;
  94. var variation = Random.NextFloat(-component.PulseVariation, component.PulseVariation) + 1;
  95. component.NextPulseTime = Timing.CurTime + GetPulseLength(component) * variation;
  96. }
  97. /// <summary>
  98. /// Begins the animation for going supercritical
  99. /// </summary>
  100. /// <param name="uid"></param>
  101. public void StartSupercriticalEvent(EntityUid uid)
  102. {
  103. // don't restart it if it's already begun
  104. if (HasComp<AnomalySupercriticalComponent>(uid))
  105. return;
  106. AdminLog.Add(LogType.Anomaly, LogImpact.Extreme, $"Anomaly {ToPrettyString(uid)} began to go supercritical.");
  107. if (_net.IsServer)
  108. Log.Info($"Anomaly is going supercritical. Entity: {ToPrettyString(uid)}");
  109. var super = AddComp<AnomalySupercriticalComponent>(uid);
  110. super.EndTime = Timing.CurTime + super.SupercriticalDuration;
  111. Appearance.SetData(uid, AnomalyVisuals.Supercritical, true);
  112. Dirty(uid, super);
  113. }
  114. /// <summary>
  115. /// Does the supercritical event for the anomaly.
  116. /// This isn't called once the anomaly reaches the point, but
  117. /// after the animation for it going supercritical
  118. /// </summary>
  119. /// <param name="uid"></param>
  120. /// <param name="component"></param>
  121. public void DoAnomalySupercriticalEvent(EntityUid uid, AnomalyComponent? component = null)
  122. {
  123. if (!Resolve(uid, ref component))
  124. return;
  125. if (!Timing.IsFirstTimePredicted)
  126. return;
  127. Audio.PlayPvs(component.SupercriticalSound, Transform(uid).Coordinates);
  128. if (_net.IsServer)
  129. Log.Info($"Raising supercritical event. Entity: {ToPrettyString(uid)}");
  130. var powerMod = 1f;
  131. if (component.CurrentBehavior != null)
  132. {
  133. var beh = _prototype.Index<AnomalyBehaviorPrototype>(component.CurrentBehavior);
  134. powerMod = beh.PulsePowerModifier;
  135. }
  136. var ev = new AnomalySupercriticalEvent(uid, powerMod);
  137. RaiseLocalEvent(uid, ref ev, true);
  138. EndAnomaly(uid, component, true, logged: true);
  139. }
  140. /// <summary>
  141. /// Ends an anomaly, cleaning up all entities that may be associated with it.
  142. /// </summary>
  143. /// <param name="uid">The anomaly being shut down</param>
  144. /// <param name="component"></param>
  145. /// <param name="supercritical">Whether or not the anomaly ended via supercritical event</param>
  146. /// <param name="spawnCore">Create anomaly cores based on the result of completing an anomaly?</param>
  147. /// <param name="logged">Whether or not the anomaly decaying/going supercritical is logged</param>
  148. public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true, bool logged = false)
  149. {
  150. if (logged)
  151. {
  152. // Logging before resolve, in case the anomaly has deleted itself.
  153. if (_net.IsServer)
  154. Log.Info($"Ending anomaly. Entity: {ToPrettyString(uid)}");
  155. AdminLog.Add(LogType.Anomaly, supercritical ? LogImpact.High : LogImpact.Low,
  156. $"Anomaly {ToPrettyString(uid)} {(supercritical ? "went supercritical" : "decayed")}.");
  157. }
  158. if (!Resolve(uid, ref component))
  159. return;
  160. var ev = new AnomalyShutdownEvent(uid, supercritical);
  161. RaiseLocalEvent(uid, ref ev, true);
  162. if (Terminating(uid) || _net.IsClient)
  163. return;
  164. if (spawnCore)
  165. {
  166. var core = Spawn(supercritical ? component.CorePrototype : component.CoreInertPrototype, Transform(uid).Coordinates);
  167. _transform.PlaceNextTo(core, uid);
  168. }
  169. if (component.DeleteEntity)
  170. QueueDel(uid);
  171. else
  172. RemCompDeferred<AnomalySupercriticalComponent>(uid);
  173. }
  174. /// <summary>
  175. /// Changes the stability of the anomaly.
  176. /// </summary>
  177. /// <param name="uid"></param>
  178. /// <param name="change"></param>
  179. /// <param name="component"></param>
  180. public void ChangeAnomalyStability(EntityUid uid, float change, AnomalyComponent? component = null)
  181. {
  182. if (!Resolve(uid, ref component))
  183. return;
  184. var newVal = component.Stability + change;
  185. component.Stability = Math.Clamp(newVal, 0, 1);
  186. Dirty(uid, component);
  187. var ev = new AnomalyStabilityChangedEvent(uid, component.Stability, component.Severity);
  188. RaiseLocalEvent(uid, ref ev, true);
  189. }
  190. /// <summary>
  191. /// Changes the severity of an anomaly, going supercritical if it exceeds 1.
  192. /// </summary>
  193. /// <param name="uid"></param>
  194. /// <param name="change"></param>
  195. /// <param name="component"></param>
  196. public void ChangeAnomalySeverity(EntityUid uid, float change, AnomalyComponent? component = null)
  197. {
  198. if (!Resolve(uid, ref component))
  199. return;
  200. var newVal = component.Severity + change;
  201. if (newVal >= 1)
  202. StartSupercriticalEvent(uid);
  203. component.Severity = Math.Clamp(newVal, 0, 1);
  204. Dirty(uid, component);
  205. var ev = new AnomalySeverityChangedEvent(uid, component.Stability, component.Severity);
  206. RaiseLocalEvent(uid, ref ev, true);
  207. }
  208. /// <summary>
  209. /// Changes the health of an anomaly, ending it if it's less than 0.
  210. /// </summary>
  211. /// <param name="uid"></param>
  212. /// <param name="change"></param>
  213. /// <param name="component"></param>
  214. public void ChangeAnomalyHealth(EntityUid uid, float change, AnomalyComponent? component = null)
  215. {
  216. if (!Resolve(uid, ref component))
  217. return;
  218. var newVal = component.Health + change;
  219. if (newVal < 0)
  220. {
  221. EndAnomaly(uid, component, logged: true);
  222. return;
  223. }
  224. component.Health = Math.Clamp(newVal, 0, 1);
  225. Dirty(uid, component);
  226. var ev = new AnomalyHealthChangedEvent(uid, component.Health);
  227. RaiseLocalEvent(uid, ref ev, true);
  228. }
  229. /// <summary>
  230. /// Gets the length of time between each pulse
  231. /// for an anomaly based on its current stability.
  232. /// </summary>
  233. /// <remarks>
  234. /// For anomalies under the instability theshold, this will return the maximum length.
  235. /// For those over the theshold, they will return an amount between the maximum and
  236. /// minium value based on a linear relationship with the stability.
  237. /// </remarks>
  238. /// <param name="component"></param>
  239. /// <returns>The length of time as a TimeSpan, not including random variation.</returns>
  240. public TimeSpan GetPulseLength(AnomalyComponent component)
  241. {
  242. DebugTools.Assert(component.MaxPulseLength > component.MinPulseLength);
  243. var modifier = Math.Clamp((component.Stability - component.GrowthThreshold) / component.GrowthThreshold, 0, 1);
  244. var lenght = (component.MaxPulseLength - component.MinPulseLength) * modifier + component.MinPulseLength;
  245. //Apply behavior modifier
  246. if (component.CurrentBehavior != null)
  247. {
  248. var behavior = _prototype.Index(component.CurrentBehavior.Value);
  249. lenght *= behavior.PulseFrequencyModifier;
  250. }
  251. return lenght;
  252. }
  253. /// <summary>
  254. /// Gets the increase in an anomaly's severity due
  255. /// to being above its growth threshold
  256. /// </summary>
  257. /// <param name="component"></param>
  258. /// <returns>The increase in severity for this anomaly</returns>
  259. private float GetSeverityIncreaseFromGrowth(AnomalyComponent component)
  260. {
  261. var score = 1 + Math.Max(component.Stability - component.GrowthThreshold, 0) * 10;
  262. return score * component.SeverityGrowthCoefficient;
  263. }
  264. public override void Update(float frameTime)
  265. {
  266. base.Update(frameTime);
  267. var anomalyQuery = EntityQueryEnumerator<AnomalyComponent>();
  268. while (anomalyQuery.MoveNext(out var ent, out var anomaly))
  269. {
  270. // if the stability is under the death threshold,
  271. // update it every second to start killing it slowly.
  272. if (anomaly.Stability < anomaly.DecayThreshold)
  273. {
  274. ChangeAnomalyHealth(ent, anomaly.HealthChangePerSecond * frameTime, anomaly);
  275. }
  276. if (Timing.CurTime > anomaly.NextPulseTime)
  277. {
  278. DoAnomalyPulse(ent, anomaly);
  279. }
  280. }
  281. var pulseQuery = EntityQueryEnumerator<AnomalyPulsingComponent>();
  282. while (pulseQuery.MoveNext(out var ent, out var pulse))
  283. {
  284. if (Timing.CurTime > pulse.EndTime)
  285. {
  286. Appearance.SetData(ent, AnomalyVisuals.IsPulsing, false);
  287. RemComp(ent, pulse);
  288. }
  289. }
  290. var supercriticalQuery = EntityQueryEnumerator<AnomalySupercriticalComponent, AnomalyComponent>();
  291. while (supercriticalQuery.MoveNext(out var ent, out var super, out var anom))
  292. {
  293. if (Timing.CurTime <= super.EndTime)
  294. continue;
  295. DoAnomalySupercriticalEvent(ent, anom);
  296. RemComp(ent, super);
  297. }
  298. }
  299. /// <summary>
  300. /// Gets random points around the anomaly based on the given parameters.
  301. /// </summary>
  302. public List<TileRef>? GetSpawningPoints(EntityUid uid, float stability, float severity, AnomalySpawnSettings settings, float powerModifier = 1f)
  303. {
  304. var xform = Transform(uid);
  305. if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
  306. return null;
  307. var amount = (int) (MathHelper.Lerp(settings.MinAmount, settings.MaxAmount, severity * stability * powerModifier) + 0.5f);
  308. var localpos = xform.Coordinates.Position;
  309. var tilerefs = _map.GetLocalTilesIntersecting(
  310. xform.GridUid.Value,
  311. grid,
  312. new Box2(localpos + new Vector2(-settings.MaxRange, -settings.MaxRange), localpos + new Vector2(settings.MaxRange, settings.MaxRange)))
  313. .ToList();
  314. if (tilerefs.Count == 0)
  315. return null;
  316. var physQuery = GetEntityQuery<PhysicsComponent>();
  317. var resultList = new List<TileRef>();
  318. while (resultList.Count < amount)
  319. {
  320. if (tilerefs.Count == 0)
  321. break;
  322. var tileref = Random.Pick(tilerefs);
  323. var distance = MathF.Sqrt(MathF.Pow(tileref.X - xform.LocalPosition.X, 2) + MathF.Pow(tileref.Y - xform.LocalPosition.Y, 2));
  324. //cut outer & inner circle
  325. if (distance > settings.MaxRange || distance < settings.MinRange)
  326. {
  327. tilerefs.Remove(tileref);
  328. continue;
  329. }
  330. if (!settings.CanSpawnOnEntities)
  331. {
  332. var valid = true;
  333. foreach (var ent in grid.GetAnchoredEntities(tileref.GridIndices))
  334. {
  335. if (!physQuery.TryGetComponent(ent, out var body))
  336. continue;
  337. if (body.BodyType != BodyType.Static ||
  338. !body.Hard ||
  339. (body.CollisionLayer & (int) CollisionGroup.Impassable) == 0)
  340. continue;
  341. valid = false;
  342. break;
  343. }
  344. if (!valid)
  345. {
  346. tilerefs.Remove(tileref);
  347. continue;
  348. }
  349. }
  350. resultList.Add(tileref);
  351. }
  352. return resultList;
  353. }
  354. }
  355. [DataRecord]
  356. public partial record struct AnomalySpawnSettings()
  357. {
  358. /// <summary>
  359. /// should entities block spawning?
  360. /// </summary>
  361. public bool CanSpawnOnEntities { get; set; } = false;
  362. /// <summary>
  363. /// The minimum number of entities that spawn per pulse
  364. /// </summary>
  365. public int MinAmount { get; set; } = 0;
  366. /// <summary>
  367. /// The maximum number of entities that spawn per pulse
  368. /// scales with severity.
  369. /// </summary>
  370. public int MaxAmount { get; set; } = 1;
  371. /// <summary>
  372. /// The distance from the anomaly in which the entities will not appear
  373. /// </summary>
  374. public float MinRange { get; set; } = 0f;
  375. /// <summary>
  376. /// The maximum radius the entities will spawn in.
  377. /// </summary>
  378. public float MaxRange { get; set; } = 1f;
  379. /// <summary>
  380. /// Whether or not anomaly spawns entities on Pulse
  381. /// </summary>
  382. public bool SpawnOnPulse { get; set; } = false;
  383. /// <summary>
  384. /// Whether or not anomaly spawns entities on SuperCritical
  385. /// </summary>
  386. public bool SpawnOnSuperCritical { get; set; } = false;
  387. /// <summary>
  388. /// Whether or not anomaly spawns entities when destroyed
  389. /// </summary>
  390. public bool SpawnOnShutdown { get; set; } = false;
  391. /// <summary>
  392. /// Whether or not anomaly spawns entities on StabilityChanged
  393. /// </summary>
  394. public bool SpawnOnStabilityChanged { get; set; } = false;
  395. /// <summary>
  396. /// Whether or not anomaly spawns entities on SeverityChanged
  397. /// </summary>
  398. public bool SpawnOnSeverityChanged { get; set; } = false;
  399. }
  400. public sealed partial class ActionAnomalyPulseEvent : InstantActionEvent { }