2019-10-11 16:57:16 -04:00
using System.Linq ;
2022-03-06 06:02:34 +13:00
using Content.Server.Administration.Logs ;
2022-04-01 15:39:26 +13:00
using Content.Server.Atmos.Components ;
2021-06-09 22:19:39 +02:00
using Content.Server.Explosion.Components ;
2022-04-01 15:39:26 +13:00
using Content.Server.NodeContainer.EntitySystems ;
2022-11-04 14:24:41 +13:00
using Content.Server.NPC.Pathfinding ;
2021-12-26 22:50:12 -08:00
using Content.Shared.Camera ;
2022-04-01 15:39:26 +13:00
using Content.Shared.Damage ;
2022-03-06 06:02:34 +13:00
using Content.Shared.Database ;
2022-04-01 15:39:26 +13:00
using Content.Shared.Explosion ;
2022-04-27 02:37:31 +12:00
using Content.Shared.GameTicking ;
2022-10-25 13:06:00 +13:00
using Content.Shared.Inventory ;
2022-04-01 15:39:26 +13:00
using Content.Shared.Throwing ;
2022-11-27 23:24:35 +13:00
using Robust.Server.GameStates ;
2022-04-01 15:39:26 +13:00
using Robust.Server.Player ;
2021-03-21 09:12:03 -07:00
using Robust.Shared.Audio ;
2022-04-01 15:39:26 +13:00
using Robust.Shared.Configuration ;
2019-10-11 16:57:16 -04:00
using Robust.Shared.Map ;
2021-03-21 09:12:03 -07:00
using Robust.Shared.Player ;
2022-04-01 15:39:26 +13:00
using Robust.Shared.Prototypes ;
2019-10-11 16:57:16 -04:00
using Robust.Shared.Random ;
2022-04-01 15:39:26 +13:00
using Robust.Shared.Utility ;
2019-10-11 16:57:16 -04:00
2022-04-01 15:39:26 +13:00
namespace Content.Server.Explosion.EntitySystems ;
public sealed partial class ExplosionSystem : EntitySystem
2019-10-11 16:57:16 -04:00
{
2022-04-01 15:39:26 +13:00
[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 ! ;
2022-11-27 23:24:35 +13:00
[Dependency] private readonly SharedAppearanceSystem _appearance = default ! ;
2022-04-01 15:39:26 +13:00
[Dependency] private readonly DamageableSystem _damageableSystem = default ! ;
[Dependency] private readonly NodeGroupSystem _nodeGroupSystem = default ! ;
2022-11-04 14:24:41 +13:00
[Dependency] private readonly PathfindingSystem _pathfindingSystem = default ! ;
2022-07-14 22:01:25 +10:00
[Dependency] private readonly SharedCameraRecoilSystem _recoilSystem = default ! ;
2022-05-28 23:41:17 -07:00
[Dependency] private readonly IAdminLogManager _adminLogger = default ! ;
2022-04-01 15:39:26 +13:00
[Dependency] private readonly ThrowingSystem _throwingSystem = default ! ;
2022-11-27 23:24:35 +13:00
[Dependency] private readonly PVSOverrideSystem _pvsSys = default ! ;
2022-04-05 19:22:35 +12:00
[Dependency] private readonly SharedTransformSystem _transformSystem = default ! ;
2022-04-01 15:39:26 +13:00
/// <summary>
/// "Tile-size" for space when there are no nearby grids to use as a reference.
/// </summary>
public const ushort DefaultTileSize = 1 ;
private AudioParams _audioParams = AudioParams . Default . WithVolume ( - 3f ) ;
/// <summary>
/// The "default" explosion prototype.
/// </summary>
/// <remarks>
/// Generally components should specify an explosion prototype via a yaml datafield, so that the yaml-linter can
/// find errors. However some components, like rogue arrows, or some commands like the admin-smite need to have
/// a "default" option specified outside of yaml data-fields. Hence this const string.
/// </remarks>
public const string DefaultExplosionPrototypeId = "Default" ;
public override void Initialize ( )
2022-03-04 13:48:01 -06:00
{
2022-04-01 15:39:26 +13:00
base . Initialize ( ) ;
2021-01-02 12:03:10 -06:00
2022-04-01 15:39:26 +13:00
DebugTools . Assert ( _prototypeManager . HasIndex < ExplosionPrototype > ( DefaultExplosionPrototypeId ) ) ;
2021-01-02 12:03:10 -06:00
2022-04-05 19:22:35 +12:00
// handled in ExplosionSystem.GridMap.cs
2022-04-01 15:39:26 +13:00
SubscribeLocalEvent < GridRemovalEvent > ( OnGridRemoved ) ;
SubscribeLocalEvent < GridStartupEvent > ( OnGridStartup ) ;
SubscribeLocalEvent < ExplosionResistanceComponent , GetExplosionResistanceEvent > ( OnGetResistance ) ;
2022-10-25 13:06:00 +13:00
// as long as explosion-resistance mice are never added, this should be fine (otherwise a mouse-hat will transfer it's power to the wearer).
SubscribeLocalEvent < ExplosionResistanceComponent , InventoryRelayedEvent < GetExplosionResistanceEvent > > ( ( e , c , ev ) = > OnGetResistance ( e , c , ev . Args ) ) ;
2022-04-01 15:39:26 +13:00
SubscribeLocalEvent < TileChangedEvent > ( OnTileChanged ) ;
2021-01-24 16:06:03 +01:00
2022-04-27 02:37:31 +12:00
SubscribeLocalEvent < RoundRestartCleanupEvent > ( OnReset ) ;
2022-04-05 19:22:35 +12:00
// Handled by ExplosionSystem.Processing.cs
SubscribeLocalEvent < MapChangedEvent > ( OnMapChanged ) ;
2022-04-01 15:39:26 +13:00
// handled in ExplosionSystemAirtight.cs
SubscribeLocalEvent < AirtightComponent , DamageChangedEvent > ( OnAirtightDamaged ) ;
SubscribeCvars ( ) ;
InitAirtightMap ( ) ;
2022-11-27 23:24:35 +13:00
InitVisuals ( ) ;
2022-04-01 15:39:26 +13:00
}
2022-04-27 02:37:31 +12:00
private void OnReset ( RoundRestartCleanupEvent ev )
{
_explosionQueue . Clear ( ) ;
2022-11-27 23:24:35 +13:00
if ( _activeExplosion ! = null )
QueueDel ( _activeExplosion . VisualEnt ) ;
2022-04-27 02:37:31 +12:00
_activeExplosion = null ;
2022-11-04 14:24:41 +13:00
_nodeGroupSystem . PauseUpdating = false ;
_pathfindingSystem . PauseUpdating = false ;
2022-04-27 02:37:31 +12:00
}
2022-04-01 15:39:26 +13:00
public override void Shutdown ( )
{
base . Shutdown ( ) ;
UnsubscribeCvars ( ) ;
2022-11-04 14:24:41 +13:00
_nodeGroupSystem . PauseUpdating = false ;
_pathfindingSystem . PauseUpdating = false ;
2022-04-01 15:39:26 +13:00
}
private void OnGetResistance ( EntityUid uid , ExplosionResistanceComponent component , GetExplosionResistanceEvent args )
{
2022-07-06 23:15:20 -04:00
args . DamageCoefficient * = component . DamageCoefficient ;
2022-04-01 15:39:26 +13:00
if ( component . Resistances . TryGetValue ( args . ExplotionPrototype , out var resistance ) )
2022-07-06 23:15:20 -04:00
args . DamageCoefficient * = resistance ;
2022-04-01 15:39:26 +13:00
}
2022-03-04 13:48:01 -06:00
2022-04-01 15:39:26 +13:00
/// <summary>
/// Given an entity with an explosive component, spawn the appropriate explosion.
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
public void TriggerExplosive ( EntityUid uid , ExplosiveComponent ? explosive = null , bool delete = true , float? totalIntensity = null , float? radius = null , EntityUid ? user = 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 ,
2022-04-05 19:22:35 +12:00
explosive . TileBreakScale ,
explosive . MaxTileBreak ,
2022-04-09 09:07:02 +12:00
explosive . CanCreateVacuum ,
2022-04-01 15:39:26 +13:00
user ) ;
if ( delete )
EntityManager . QueueDeleteEntity ( uid ) ;
}
/// <summary>
/// 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".
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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 ) ;
}
/// <summary>
/// Inverse formula for <see cref="RadiusToIntensity"/>
/// </summary>
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 )
2021-01-24 16:06:03 +01:00
{
2022-04-01 15:39:26 +13:00
// maxIntensity is a non-issue, can use simple inverse formula
return MathF . Cbrt ( 3 * totalIntensity / ( slope * MathF . PI ) ) ;
2022-03-06 06:02:34 +13:00
}
2022-03-04 13:48:01 -06:00
2022-04-01 15:39:26 +13:00
return r0 * ( MathF . Sqrt ( 12 * totalIntensity / v0 - 3 ) / 6 + 0.5f ) ;
}
/// <summary>
/// Queue an explosions, centered on some entity.
/// </summary>
public void QueueExplosion ( EntityUid uid ,
string typeId ,
float totalIntensity ,
float slope ,
float maxTileIntensity ,
2022-04-05 19:22:35 +12:00
float tileBreakScale = 1f ,
int maxTileBreak = int . MaxValue ,
2022-04-09 09:07:02 +12:00
bool canCreateVacuum = true ,
2022-04-01 15:39:26 +13:00
EntityUid ? user = null ,
2023-02-10 17:45:38 -06:00
bool addLog = true )
2022-04-01 15:39:26 +13:00
{
2023-02-10 17:45:38 -06:00
var pos = Transform ( uid ) ;
2022-04-01 15:39:26 +13:00
2023-02-10 17:45:38 -06:00
QueueExplosion ( pos . MapPosition , typeId , totalIntensity , slope , maxTileIntensity , tileBreakScale , maxTileBreak , canCreateVacuum , addLog : false ) ;
2022-04-01 15:39:26 +13:00
if ( ! addLog )
return ;
if ( user = = null )
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Explosion , LogImpact . High ,
2023-02-10 17:45:38 -06:00
$"{ToPrettyString(uid):entity} exploded ({typeId}) at {pos.Coordinates:coordinates} with intensity {totalIntensity} slope {slope}" ) ;
2022-04-01 15:39:26 +13:00
else
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Explosion , LogImpact . High ,
2023-02-10 17:45:38 -06:00
$"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at {pos.Coordinates:coordinates} with intensity {totalIntensity} slope {slope}" ) ;
2022-04-01 15:39:26 +13:00
}
/// <summary>
/// Queue an explosion, with a specified epicenter and set of starting tiles.
/// </summary>
public void QueueExplosion ( MapCoordinates epicenter ,
string typeId ,
float totalIntensity ,
float slope ,
float maxTileIntensity ,
2022-04-05 19:22:35 +12:00
float tileBreakScale = 1f ,
int maxTileBreak = int . MaxValue ,
2022-04-09 09:07:02 +12:00
bool canCreateVacuum = true ,
2023-02-10 17:45:38 -06:00
bool addLog = true )
2022-04-01 15:39:26 +13:00
{
if ( totalIntensity < = 0 | | slope < = 0 )
return ;
if ( ! _prototypeManager . TryIndex < ExplosionPrototype > ( typeId , out var type ) )
2022-03-06 06:02:34 +13:00
{
2022-04-01 15:39:26 +13:00
Logger . Error ( $"Attempted to spawn unknown explosion prototype: {type}" ) ;
return ;
2022-03-06 06:02:34 +13:00
}
2022-03-04 13:48:01 -06:00
2022-04-01 15:39:26 +13:00
if ( addLog ) // dont log if already created a separate, more detailed, log.
2023-02-10 17:45:38 -06:00
_adminLogger . Add ( LogType . Explosion , LogImpact . High , $"Explosion ({typeId}) spawned at {epicenter:coordinates} with intensity {totalIntensity} slope {slope}" ) ;
2022-05-28 23:41:17 -07:00
2022-04-01 15:39:26 +13:00
_explosionQueue . Enqueue ( ( ) = > SpawnExplosion ( epicenter , type , totalIntensity ,
2022-04-09 09:07:02 +12:00
slope , maxTileIntensity , tileBreakScale , maxTileBreak , canCreateVacuum ) ) ;
2022-04-01 15:39:26 +13:00
}
/// <summary>
/// This function actually spawns the explosion. It returns an <see cref="Explosion"/> instance with
/// information about the affected tiles for the explosion system to process. It will also trigger the
/// camera shake and sound effect.
/// </summary>
private Explosion ? SpawnExplosion ( MapCoordinates epicenter ,
ExplosionPrototype type ,
float totalIntensity ,
float slope ,
2022-04-05 19:22:35 +12:00
float maxTileIntensity ,
float tileBreakScale ,
2022-04-09 09:07:02 +12:00
int maxTileBreak ,
bool canCreateVacuum )
2022-04-01 15:39:26 +13:00
{
2022-04-05 19:22:35 +12:00
if ( ! _mapManager . MapExists ( epicenter . MapId ) )
return null ;
2022-04-01 15:39:26 +13:00
var results = GetExplosionTiles ( epicenter , type . ID , totalIntensity , slope , maxTileIntensity ) ;
if ( results = = null )
return null ;
var ( area , iterationIntensity , spaceData , gridData , spaceMatrix ) = results . Value ;
2022-11-27 23:24:35 +13:00
var visualEnt = CreateExplosionVisualEntity ( epicenter , type . ID , spaceMatrix , spaceData , gridData . Values , iterationIntensity ) ;
2022-04-01 15:39:26 +13:00
// 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 ) ;
2022-06-12 19:45:47 -04:00
SoundSystem . Play ( type . Sound . GetSound ( ) , filter , mapEntityCoords , _audioParams ) ;
2022-04-01 15:39:26 +13:00
return new Explosion ( this ,
type ,
spaceData ,
gridData . Values . ToList ( ) ,
iterationIntensity ,
epicenter ,
spaceMatrix ,
area ,
2022-04-05 19:22:35 +12:00
tileBreakScale ,
maxTileBreak ,
2022-04-09 09:07:02 +12:00
canCreateVacuum ,
2022-04-01 15:39:26 +13:00
EntityManager ,
2022-11-27 23:24:35 +13:00
_mapManager ,
visualEnt ) ;
2022-04-01 15:39:26 +13:00
}
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 )
2022-03-06 06:02:34 +13:00
{
2022-04-01 15:39:26 +13:00
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 ) ;
2021-01-02 12:03:10 -06:00
}
2019-10-11 16:57:16 -04:00
}
}