using System.Linq;
using Content.Server.Atmos.Components;
using Content.Server.Explosion.Components;
using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Camera;
using Content.Shared.Damage;
using Content.Shared.Explosion;
using Robust.Server.Containers;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class ExplosionSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly NodeGroupSystem _nodeGroupSystem = default!;
[Dependency] private readonly CameraRecoilSystem _recoilSystem = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
///
/// "Tile-size" for space when there are no nearby grids to use as a reference.
///
public const ushort DefaultTileSize = 1;
private AudioParams _audioParams = AudioParams.Default.WithVolume(-3f);
public override void Initialize()
{
base.Initialize();
// handled in ExplosionSystemGridMap.cs
SubscribeLocalEvent(OnGridRemoved);
SubscribeLocalEvent(OnGridStartup);
SubscribeLocalEvent(OnGetResistance);
_mapManager.TileChanged += OnTileChanged;
// handled in ExplosionSystemAirtight.cs
SubscribeLocalEvent(OnAirtightDamaged);
SubscribeCvars();
}
public override void Shutdown()
{
base.Shutdown();
_mapManager.TileChanged -= OnTileChanged;
UnsubscribeCvars();
}
private void OnGetResistance(EntityUid uid, ExplosionResistanceComponent component, GetExplosionResistanceEvent args)
{
args.Resistance += component.GlobalResistance;
if (component.Resistances.TryGetValue(args.ExplotionPrototype, out var resistance))
args.Resistance += resistance;
}
///
/// Given an entity with an explosive component, spawn the appropriate explosion.
///
///
/// Also accepts radius or intensity arguments. This is useful for explosives where the intensity is not
/// specified in the yaml / by the component, but determined dynamically (e.g., by the quantity of a
/// solution in a reaction).
///
public void TriggerExplosive(EntityUid uid, ExplosiveComponent? explosive = null, bool delete = true, float? totalIntensity = null, float? radius = null)
{
// log missing: false, because some entities (e.g. liquid tanks) attempt to trigger explosions when damaged,
// but may not actually be explosive.
if (!Resolve(uid, ref explosive, logMissing: false))
return;
// No reusable explosions here.
if (explosive.Exploded)
return;
explosive.Exploded = true;
// Override the explosion intensity if optional arguments were provided.
if (radius != null)
totalIntensity ??= RadiusToIntensity((float) radius, explosive.IntensitySlope, explosive.MaxIntensity);
totalIntensity ??= explosive.TotalIntensity;
QueueExplosion(uid,
explosive.ExplosionType,
(float) totalIntensity,
explosive.IntensitySlope,
explosive.MaxIntensity);
if (delete)
EntityManager.QueueDeleteEntity(uid);
}
///
/// Find the strength needed to generate an explosion of a given radius. More useful for radii larger then 4, when the explosion becomes less "blocky".
///
///
/// This assumes the explosion is in a vacuum / unobstructed. Given that explosions are not perfectly
/// circular, here radius actually means the sqrt(Area/pi), where the area is the total number of tiles
/// covered by the explosion. Until you get to radius 30+, this is functionally equivalent to the
/// actual radius.
///
public float RadiusToIntensity(float radius, float slope, float maxIntensity = 0)
{
// If you consider the intensity at each tile in an explosion to be a height. Then a circular explosion is
// shaped like a cone. So total intensity is like the volume of a cone with height = slope * radius. Of
// course, as the explosions are not perfectly circular, this formula isn't perfect, but the formula works
// reasonably well.
// This should actually use the formula for the volume of a distorted octagonal frustum. But this is good
// enough.
var coneVolume = slope * MathF.PI / 3 * MathF.Pow(radius, 3);
if (maxIntensity <= 0 || slope * radius < maxIntensity)
return coneVolume;
// This explosion is limited by the maxIntensity.
// Instead of a cone, we have a conical frustum.
// Subtract the volume of the missing cone segment, with height:
var h = slope * radius - maxIntensity;
return coneVolume - h * MathF.PI / 3 * MathF.Pow(h / slope, 2);
}
///
/// Inverse formula for
///
public float IntensityToRadius(float totalIntensity, float slope, float maxIntensity)
{
// max radius to avoid being capped by max-intensity
var r0 = maxIntensity / slope;
// volume at r0
var v0 = RadiusToIntensity(r0, slope);
if (totalIntensity <= v0)
{
// maxIntensity is a non-issue, can use simple inverse formula
return MathF.Cbrt(3 * totalIntensity / (slope * MathF.PI));
}
return r0 * (MathF.Sqrt(12 * totalIntensity/ v0 - 3) / 6 + 0.5f);
}
///
/// Queue an explosions, centered on some entity.
///
public void QueueExplosion(EntityUid uid,
string typeId,
float intensity,
float slope,
float maxTileIntensity)
{
QueueExplosion(Transform(uid).MapPosition, typeId, intensity, slope, maxTileIntensity);
}
///
/// Queue an explosion, with a specified epicenter and set of starting tiles.
///
public void QueueExplosion(MapCoordinates epicenter,
string typeId,
float totalIntensity,
float slope,
float maxTileIntensity)
{
if (totalIntensity <= 0 || slope <= 0)
return;
if (!_prototypeManager.TryIndex(typeId, out var type))
{
Logger.Error($"Attempted to spawn unknown explosion prototype: {type}");
return;
}
_explosionQueue.Enqueue(() => SpawnExplosion(epicenter, type, totalIntensity,
slope, maxTileIntensity));
}
///
/// This function actually spawns the explosion. It returns an instance with
/// information about the affected tiles for the explosion system to process. It will also trigger the
/// camera shake and sound effect.
///
private Explosion? SpawnExplosion(MapCoordinates epicenter,
ExplosionPrototype type,
float totalIntensity,
float slope,
float maxTileIntensity)
{
var results = GetExplosionTiles(epicenter, type.ID, totalIntensity, slope, maxTileIntensity);
if (results == null)
return null;
var (area, iterationIntensity, spaceData, gridData, spaceMatrix) = results.Value;
RaiseNetworkEvent(GetExplosionEvent(epicenter, type.ID, spaceMatrix, spaceData, gridData.Values, iterationIntensity));
// camera shake
CameraShake(iterationIntensity.Count * 2.5f, epicenter, totalIntensity);
//For whatever bloody reason, sound system requires ENTITY coordinates.
var mapEntityCoords = EntityCoordinates.FromMap(EntityManager, _mapManager.GetMapEntityId(epicenter.MapId), epicenter);
// play sound.
var audioRange = iterationIntensity.Count * 5;
var filter = Filter.Pvs(epicenter).AddInRange(epicenter, audioRange);
SoundSystem.Play(filter, type.Sound.GetSound(), mapEntityCoords, _audioParams);
return new Explosion(this,
type,
spaceData,
gridData.Values.ToList(),
iterationIntensity,
epicenter,
spaceMatrix,
area
);
}
///
/// Constructor for the shared using the server-exclusive explosion classes.
///
internal ExplosionEvent GetExplosionEvent(MapCoordinates epicenter, string id, Matrix3 spaceMatrix, SpaceExplosion? spaceData, IEnumerable gridData, List iterationIntensity)
{
var spaceTiles = spaceData?.TileLists;
Dictionary>> tileLists = new();
foreach (var grid in gridData)
{
tileLists.Add(grid.Grid.Index, grid.TileLists);
}
return new ExplosionEvent(_explosionCounter, epicenter, id, iterationIntensity, spaceTiles, tileLists, spaceMatrix);
}
private void CameraShake(float range, MapCoordinates epicenter, float totalIntensity)
{
var players = Filter.Empty();
players.AddInRange(epicenter, range, _playerManager, EntityManager);
foreach (var player in players.Recipients)
{
if (player.AttachedEntity is not EntityUid uid)
continue;
var playerPos = Transform(player.AttachedEntity!.Value).WorldPosition;
var delta = epicenter.Position - playerPos;
if (delta.EqualsApprox(Vector2.Zero))
delta = new(0.01f, 0);
var distance = delta.Length;
var effect = 5 * MathF.Pow(totalIntensity, 0.5f) * (1 - distance / range);
if (effect > 0.01f)
_recoilSystem.KickCamera(uid, -delta.Normalized * effect);
}
}
}