using System.Linq; using System.Numerics; using Content.Shared.Camera; using Robust.Shared.Noise; using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Shared._ES.Camera; /// /// Handles sending rotational or translational screenshake to an entity, managing the screenshake commands /// of every entity currently screenshaking, and setting offset/rotation when updated /// // DeltaV - renamed from ESScreenshakeSystem to SharedESScreenshakeSystem public sealed class SharedESScreenshakeSystem : EntitySystem { // [Dependency] private readonly IRobustRandom _random = default!; // Delta V - Never used [Dependency] private readonly IGameTiming _timing = default!; #region Internal public override void Initialize() { base.Initialize(); // SubscribeLocalEvent(OnGetEyeRotation); // DeltaV - Moved to Client system // SubscribeLocalEvent(OnGetEyeOffset); // DeltaV - Moved to Client system SubscribeLocalEvent(OnEntityUnpaused); } public override void Update(float frameTime) { base.Update(frameTime); // TODO mirror might make sense to never remove individual commands and only remove the comp if theyre all > calculatedend instead. var shakers = EntityQueryEnumerator(); while (shakers.MoveNext(out var ent, out var eye, out var shake)) { if (shake.Commands.Count == 0) { RemCompDeferred(ent); continue; } foreach (var command in shake.Commands.ToList()) { // handle removing old commands if (_timing.CurTime < command.CalculatedEnd) continue; shake.Commands.Remove(command); Dirty(ent, shake); } } } private void OnEntityUnpaused(Entity ent, ref EntityUnpausedEvent args) { // rebuild screenshake commands but with offset times var newSet = new HashSet(); foreach (var command in ent.Comp.Commands) { var newCommand = command with { CalculatedEnd = command.CalculatedEnd + args.PausedTime, Start = command.Start + args.PausedTime, }; newSet.Add(newCommand); } ent.Comp.Commands = newSet; Dirty(ent); } /// /// Calculates when both traumas will be at least = 0 given the decay rate and start time. /// private TimeSpan CalculateEndTimeForCommand(Entity ent, ESScreenshakeParameters? translation, ESScreenshakeParameters? rotation, TimeSpan start) { // https://www.desmos.com/calculator/optip8eucx var secsUntilRotationalEnd = rotation != null ? MathF.Sqrt(rotation.Trauma / rotation.DecayRate) : 0f; var secsUntilTranslationalEnd = translation != null ? MathF.Sqrt(translation.Trauma / translation.DecayRate) : 0f; var larger = secsUntilTranslationalEnd >= secsUntilRotationalEnd ? secsUntilTranslationalEnd : secsUntilRotationalEnd; return start + TimeSpan.FromSeconds(larger); } /// /// Gets the trauma value for the current time, given the decay rate and start time. /// // DeltaV - make public public float CalculateTraumaValueForCurrentTime(ESScreenshakeParameters parameters, TimeSpan start) { var timeDiff = _timing.CurTime - start; // erm if (timeDiff < TimeSpan.Zero) return 0f; // trauma decays quadratically with seconds passed // https://www.desmos.com/calculator/optip8eucx var totalSecsSquared = (float) (timeDiff.TotalSeconds * timeDiff.TotalSeconds); return -totalSecsSquared * parameters.DecayRate + parameters.Trauma; } #endregion #region Public API public void Screenshake(EntityUid uid, ESScreenshakeParameters? translation, ESScreenshakeParameters? rotation) { if (!HasComp(uid)) return; var comp = EnsureComp(uid); var time = _timing.CurTime; var end = CalculateEndTimeForCommand((uid, comp), translation, rotation, time); var command = new ESScreenshakeCommand(translation, rotation, time, end); comp.Commands.Add(command); Dirty(uid, comp); } public void Screenshake(Filter filter, ESScreenshakeParameters? translation, ESScreenshakeParameters? rotation) { foreach (var player in filter.Recipients) { if (player.AttachedEntity is {} ent) Screenshake(ent, translation, rotation); } } #endregion }