using Content.Shared.Explosion;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Explosion;
///
/// This system is responsible for showing the client-side explosion effects (light source & fire-overlay). The
/// fire overlay code is just a bastardized version of the atmos plasma fire overlay and uses the same texture.
///
public sealed class ExplosionOverlaySystem : EntitySystem
{
private ExplosionOverlay _overlay = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
///
/// For how many seconds should an explosion stay on-screen once it has finished expanding?
///
public const float ExplosionPersistence = 0.3f;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent(OnExplosion);
SubscribeNetworkEvent(HandleExplosionUpdate);
var overlayManager = IoCManager.Resolve();
_overlay = new ExplosionOverlay();
if (!overlayManager.HasOverlay())
overlayManager.AddOverlay(_overlay);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
// increment the lifetime of completed explosions, and remove them if they have been ons screen for more
// than ExplosionPersistence seconds
foreach (var explosion in _overlay.CompletedExplosions.ToArray())
{
explosion.Lifetime += frameTime;
if (explosion.Lifetime >= ExplosionPersistence)
{
EntityManager.QueueDeleteEntity(explosion.LightEntity);
_overlay.CompletedExplosions.Remove(explosion);
}
}
}
///
/// The server has processed some explosion. This updates the client-side overlay so that the area covered
/// by the fire-visual matches up with the area that the explosion has affected.
///
private void HandleExplosionUpdate(ExplosionOverlayUpdateEvent args)
{
if (args.ExplosionId != _overlay.ActiveExplosion?.Explosionid && !IsNewer(args.ExplosionId))
{
// out of order events. Ignore.
return;
}
_overlay.Index = args.Index;
if (_overlay.ActiveExplosion == null)
{
// no active explosion... events out of order?
return;
}
if (args.Index != int.MaxValue)
return;
// the explosion has finished expanding
_overlay.Index = 0;
_overlay.CompletedExplosions.Add(_overlay.ActiveExplosion);
_overlay.ActiveExplosion = null;
}
///
/// A new explosion occurred. This prepares the client-side light entity and stores the
/// explosion/fire-effect overlay data.
///
private void OnExplosion(ExplosionEvent args)
{
if (!_protoMan.TryIndex(args.TypeID, out ExplosionPrototype? type))
return;
// spawn in a light source at the epicenter
var lightEntity = Spawn("ExplosionLight", args.Epicenter);
var light = EnsureComp(lightEntity);
light.Energy = light.Radius = args.Intensity.Count;
light.Color = type.LightColor;
if (_overlay.ActiveExplosion == null)
{
_overlay.ActiveExplosion = new(args, type, lightEntity);
return;
}
// we have a currently active explosion. Can happen when events are received out of order. either multiple
// explosions are happening in one tick, or a new explosion was received before the event telling us the old one
// finished got through.
if (IsNewer(args.ExplosionId))
{
// This is a newer explosion. Add the old-currently-active explosions to the completed list
_overlay.CompletedExplosions.Add(_overlay.ActiveExplosion);
_overlay.ActiveExplosion = new(args, type, lightEntity);
}
else
{
// explosions were out of order. keep the active one, and directly add the received one to the completed
// list.
_overlay.CompletedExplosions.Add(new(args, type, lightEntity));
return;
}
}
public bool IsNewer(byte explosionId)
{
if (_overlay.ActiveExplosion == null)
return true;
// byte goes up to 255, then wraps back to zero.
// what are the odds of more than 128 explosions happening in a tick?
return _overlay.ActiveExplosion.Explosionid < explosionId
|| _overlay.ActiveExplosion.Explosionid > 192 && explosionId < 64;
}
public override void Shutdown()
{
base.Shutdown();
var overlayManager = IoCManager.Resolve();
if (overlayManager.HasOverlay())
overlayManager.RemoveOverlay();
}
}
internal sealed class Explosion
{
public Dictionary>? SpaceTiles;
public Dictionary>> Tiles;
public List Intensity;
public EntityUid LightEntity;
public MapId Map;
public byte Explosionid;
public Matrix3 SpaceMatrix;
///
/// How long have we been drawing this explosion, starting from the time the explosion was fully drawn.
///
public float Lifetime;
///
/// The textures used for the explosion fire effect. Each fire-state is associated with an explosion
/// intensity range, and each stat itself has several textures.
///
public List FireFrames = new();
public Color? FireColor;
internal Explosion(ExplosionEvent args, ExplosionPrototype type, EntityUid lightEntity)
{
Map = args.Epicenter.MapId;
SpaceTiles = args.SpaceTiles;
Tiles = args.Tiles;
Intensity = args.Intensity;
SpaceMatrix = args.SpaceMatrix;
Explosionid = args.ExplosionId;
FireColor = type.FireColor;
LightEntity = lightEntity;
var fireRsi = IoCManager.Resolve().GetResource(type.TexturePath).RSI;
foreach (var state in fireRsi)
{
FireFrames.Add(state.GetFrames(RSI.State.Direction.South));
if (FireFrames.Count == type.FireStates)
break;
}
}
}