using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using System; using System.Collections.Generic; using Robust.Client.Player; using Robust.Shared.GameObjects; using Robust.Shared.Log; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.Enums; namespace Robust.Client.GameObjects { public sealed class EffectSystem : EntitySystem { [Dependency] private readonly IGameTiming gameTiming = default!; [Dependency] private readonly IResourceCache resourceCache = default!; [Dependency] private readonly IEyeManager eyeManager = default!; [Dependency] private readonly IOverlayManager overlayManager = default!; [Dependency] private readonly IPrototypeManager prototypeManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; private readonly List _Effects = new(); public override void Initialize() { base.Initialize(); SubscribeNetworkEvent(CreateEffect); SubscribeLocalEvent(CreateEffect); var overlay = new EffectOverlay(this, prototypeManager, _playerManager, EntityManager); overlayManager.AddOverlay(overlay); } public override void Shutdown() { base.Shutdown(); overlayManager.RemoveOverlay(typeof(EffectOverlay)); } public void CreateEffect(EffectSystemMessage message) { // The source of effects is either local actions during FirstTimePredicted, or the network at LastServerTick // When replaying predicted input, don't spam effects. if(gameTiming.InPrediction && !gameTiming.IsFirstTimePredicted) return; if (message.AttachedEntityUid != null && message.Coordinates != default) { Logger.Warning("Set both an AttachedEntityUid and EntityCoordinates on an EffectSystemMessage for sprite {0} which is not supported!", message.EffectSprite); } if (message.LifeTime <= TimeSpan.Zero) { Logger.Warning("Effect using sprite {0} had zero lifetime.", message.EffectSprite); return; } //Create effect from creation message var effect = new Effect(message, resourceCache, _mapManager, EntityManager); effect.Deathtime = gameTiming.CurTime + message.LifeTime; _Effects.Add(effect); } public override void FrameUpdate(float frameTime) { var curTime = gameTiming.CurTime; for (int i = 0; i < _Effects.Count; i++) { var effect = _Effects[i]; //These effects have died // Effects are purely visual, so they don't need to be ran through prediction. // once CurTime ever passes DeathTime (clients render the top at IsFirstTimePredicted, where this happens) just remove them. if (curTime > effect.Deathtime) { //Remove from the effects list and decrement the iterator _Effects.Remove(effect); i--; } else { //Update variables of the effect via its deltas effect.Update(frameTime); } } } private sealed class Effect { /// /// Effect Sprite /// This is the sprite that will be drawn as the "effect". /// public Texture EffectSprite { get; set; } public RSI.State? RsiState { get; set; } public int AnimationIndex { get; set; } public float AnimationTime { get; set; } public bool AnimationLoops { get; set; } /// /// Entity that the effect is attached to /// public EntityUid? AttachedEntityUid { get; } /// /// Offset relative to the attached entity /// public Vector2 AttachedOffset { get; } /// /// Effect position relative to the emit position /// public EntityCoordinates Coordinates; /// /// Where the emitter was when the effect was first emitted /// public EntityCoordinates EmitterCoordinates; /// /// Effect's x/y velocity /// public Vector2 Velocity = Vector2.Zero; /// /// Effect's x/y acceleration /// public Vector2 Acceleration = Vector2.Zero; /// /// Effect's radial velocity - relative to EmitterPosition /// public float RadialVelocity = 0f; /// /// Effect's radial acceleration /// public float RadialAcceleration = 0f; /// /// Effect's tangential velocity - relative to EmitterPosition /// public float TangentialVelocity = 0f; /// /// Effect's tangential acceleration /// public float TangentialAcceleration = 0f; /// /// Effect's spin about its center in radians /// public float Rotation = 0f; /// /// Rate of change of effect's spin /// public float RotationRate = 0f; /// /// Effect's current size /// public Vector2 Size = new(1f, 1f); /// /// Rate of change of effect's size change /// public float SizeDelta = 0f; /// /// Effect's current color /// public Vector4 Color = new(255, 255, 255, 255); /// /// Rate of change of effect's color /// public Vector4 ColorDelta = new(0, 0, 0, 0); /// /// True if the effect is affected by lighting. /// public bool Shaded = true; /// /// CurTime after which the effect will "die" /// public TimeSpan Deathtime; private readonly IMapManager _mapManager; private readonly IEntityManager _entityManager; public Effect(EffectSystemMessage effectcreation, IResourceCache resourceCache, IMapManager mapManager, IEntityManager entityManager) { if (effectcreation.RsiState != null) { var rsi = resourceCache .GetResource(new ResourcePath("/Textures/") / effectcreation.EffectSprite) .RSI; RsiState = rsi[effectcreation.RsiState]; EffectSprite = RsiState.Frame0; } else { EffectSprite = resourceCache .GetResource(new ResourcePath("/Textures/") / effectcreation.EffectSprite) .Texture; } AnimationLoops = effectcreation.AnimationLoops; AttachedEntityUid = effectcreation.AttachedEntityUid; AttachedOffset = effectcreation.AttachedOffset; Coordinates = effectcreation.Coordinates; EmitterCoordinates = effectcreation.EmitterCoordinates; Velocity = effectcreation.Velocity; Acceleration = effectcreation.Acceleration; RadialVelocity = effectcreation.RadialVelocity; RadialAcceleration = effectcreation.RadialAcceleration; TangentialVelocity = effectcreation.TangentialVelocity; TangentialAcceleration = effectcreation.TangentialAcceleration; Rotation = effectcreation.Rotation; RotationRate = effectcreation.RotationRate; Size = effectcreation.Size; SizeDelta = effectcreation.SizeDelta; Color = effectcreation.Color; ColorDelta = effectcreation.ColorDelta; Shaded = effectcreation.Shaded; _mapManager = mapManager; _entityManager = entityManager; } public void Update(float frameTime) { Velocity += Acceleration * frameTime; RadialVelocity += RadialAcceleration * frameTime; TangentialVelocity += TangentialAcceleration * frameTime; var deltaPosition = new Vector2(0f, 0f); //If we have an emitter we can do special effects around that emitter position if (_mapManager.GridExists(EmitterCoordinates.GetGridId(_entityManager))) { //Calculate delta p due to radial velocity var positionRelativeToEmitter = Coordinates.ToMapPos(_entityManager) - EmitterCoordinates.ToMapPos(_entityManager); var deltaRadial = RadialVelocity * frameTime; deltaPosition = positionRelativeToEmitter * (deltaRadial / positionRelativeToEmitter.Length); //Calculate delta p due to tangential velocity var radius = positionRelativeToEmitter.Length; if (radius > 0) { var theta = (float) Math.Atan2(positionRelativeToEmitter.Y, positionRelativeToEmitter.X); theta += TangentialVelocity * frameTime; deltaPosition += new Vector2(radius * (float) Math.Cos(theta), radius * (float) Math.Sin(theta)) - positionRelativeToEmitter; } } //Calculate new position from our velocity as well as possible rotation/movement around emitter deltaPosition += Velocity * frameTime; Coordinates = Coordinates.Offset(deltaPosition); //Finish calculating new rotation, size, color Rotation += RotationRate * frameTime; Size += SizeDelta * frameTime; Color += ColorDelta * frameTime; if (RsiState == null) { return; } // Calculate RSI animations. var delayCount = RsiState.DelayCount; if (delayCount > 0 && (AnimationLoops || AnimationIndex < delayCount - 1)) { AnimationTime += frameTime; while (RsiState.GetDelay(AnimationIndex) < AnimationTime) { var delay = RsiState.GetDelay(AnimationIndex); AnimationIndex += 1; AnimationTime -= delay; if (AnimationIndex == delayCount) { if (AnimationLoops) { AnimationIndex = 0; } else { break; } } EffectSprite = RsiState.GetFrame(RSI.State.Direction.South, AnimationIndex); } } } } private static Color ToColor(Vector4 color) { color = Vector4.Clamp(color / 255f, Vector4.Zero, Vector4.One); return new Color(color.X, color.Y, color.Z, color.W); } private sealed class EffectOverlay : Overlay { private readonly IPlayerManager _playerManager; public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; private readonly ShaderInstance _unshadedShader; private readonly EffectSystem _owner; private readonly IEntityManager _entityManager; public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IPlayerManager playerMan, IEntityManager entityManager) { _owner = owner; _unshadedShader = protoMan.Index("unshaded").Instance(); _playerManager = playerMan; _entityManager = entityManager; } protected internal override void Draw(in OverlayDrawArgs args) { var map = _owner.eyeManager.CurrentMap; var worldHandle = args.WorldHandle; if (_playerManager.LocalPlayer?.ControlledEntity is not {} playerEnt) return; var playerXform = _entityManager.GetComponent(playerEnt); foreach (var effect in _owner._Effects) { TransformComponent? attachedXform = null; if ((effect.AttachedEntityUid is {} attached && _entityManager.TryGetComponent(attached, out attachedXform) && attachedXform.MapID != playerXform.MapID) || (effect.AttachedEntityUid == null && effect.Coordinates.GetMapId(_entityManager) != map)) { continue; } if (!effect.Shaded) worldHandle.UseShader(_unshadedShader); // TODO: Should be doing matrix transformations var effectSprite = effect.EffectSprite; var coordinates = (attachedXform?.Coordinates ?? effect.Coordinates) .Offset(effect.AttachedOffset); // If we've never seen the entity before then can't resolve coordinates. if (!coordinates.IsValid(_entityManager)) continue; // ??? var rotation = attachedXform?.WorldRotation ?? _entityManager.GetComponent(coordinates.EntityId).WorldRotation; var effectOrigin = coordinates.ToMapPos(_entityManager); var effectArea = Box2.CenteredAround(effectOrigin, effect.Size); var rotatedBox = new Box2Rotated(effectArea, effect.Rotation + rotation, effectOrigin); worldHandle.DrawTextureRect(effectSprite, rotatedBox, ToColor(effect.Color)); if (!effect.Shaded) worldHandle.UseShader(null); } } } } }