@@ -1,4 +1,4 @@
using System ;
using System ;
using System.Collections.Generic ;
using System.Linq ;
using Content.Server.Camera ;
@@ -21,9 +21,9 @@ using Robust.Shared.Player;
using Robust.Shared.Random ;
using Robust.Shared.Timing ;
namespace Content.Server.Explosion
namespace Content.Server.Explosion.EntitySystems
{
public static class ExplosionHelper
public class ExplosionSystem : EntitySystem
{
/// <summary>
/// Distance used for camera shake when distance from explosion is (0.0, 0.0).
@@ -34,19 +34,35 @@ namespace Content.Server.Explosion
/// <summary>
/// Chance of a tile breaking if the severity is Light and Heavy
/// </summary>
private static readonly float LightBreakChance = 0.3f ;
private static readonly float HeavyBreakChance = 0.8f ;
private static SoundSpecifier _explosionSound = new SoundCollectionSpecifier ( "explosion" ) ;
private const float LightBreakChance = 0.3f ;
private const float HeavyBreakChance = 0.8f ;
private static bool IgnoreExplosivePassable ( IEntity e ) = > e . HasTag ( "ExplosivePassable" ) ;
// TODO move this to the component
private static readonly SoundSpecifier ExplosionSound = new SoundCollectionSpecifier ( "explosion" ) ;
private static ExplosionSeverity CalculateSeverity ( float distance , float devastationRange , float heaveyRange )
[Dependency] private readonly IEntityLookup _entityLookup = default ! ;
[Dependency] private readonly IGameTiming _timing = default ! ;
[Dependency] private readonly IMapManager _maps = default ! ;
[Dependency] private readonly IPlayerManager _players = default ! ;
[Dependency] private readonly IRobustRandom _random = default ! ;
[Dependency] private readonly ITileDefinitionManager _tiles = default ! ;
[Dependency] private readonly ActSystem _acts = default ! ;
[Dependency] private readonly EffectSystem _effects = default ! ;
[Dependency] private readonly TriggerSystem _triggers = default ! ;
private bool IgnoreExplosivePassable ( IEntity e )
{
return e . HasTag ( "ExplosivePassable" ) ;
}
private ExplosionSeverity CalculateSeverity ( float distance , float devastationRange , float heavyRange )
{
if ( distance < devastationRange )
{
return ExplosionSeverity . Destruction ;
}
else if ( distance < heave yRange )
else if ( distance < heavyRange )
{
return ExplosionSeverity . Heavy ;
}
@@ -56,174 +72,10 @@ namespace Content.Server.Explosion
}
}
/// <summary>
/// Damage entities inside the range. The damage depends on a discrete
/// damage bracket [light, heavy, devastation] and the distance from the epicenter
/// </summary>
/// <returns>
/// A dictionary of coordinates relative to the parents of every grid of entities that survived the explosion,
/// have an airtight component and are currently blocking air. Like a wall.
/// </returns>
private static void DamageEntitiesInRange ( EntityCoordinates epicenter , Box2 boundingBox ,
float devastationRange ,
float heaveyRange ,
float maxRange ,
MapId mapId )
private void CameraShakeInRange ( EntityCoordinates epicenter , float maxRange )
{
var entityManag er = IoCManag er. Resolv e< IEntityMana ger > ( ) ;
var play ers = _play ers . GetPlayersInRang e( epicenter , ( int ) Math . Ceiling ( maxRan ge) ) ;
var exAct = EntitySystem . Get < ActSystem > ( ) ;
var entitiesInRange = IoCManager . Resolve < IEntityLookup > ( ) . GetEntitiesInRange ( mapId , boundingBox , 0 ) . ToList ( ) ;
var impassableEntities = new List < Tuple < IEntity , float > > ( ) ;
var nonImpassableEntities = new List < Tuple < IEntity , float > > ( ) ;
// TODO: Given this seems to rely on physics it should just query directly like everything else.
// The entities are paired with their distance to the epicenter
// and splitted into two lists based on if they are Impassable or not
foreach ( var entity in entitiesInRange )
{
if ( entity . Deleted | | entity . IsInContainer ( ) )
{
continue ;
}
if ( ! entity . Transform . Coordinates . TryDistance ( entityManager , epicenter , out var distance ) | | distance > maxRange )
{
continue ;
}
if ( ! entity . TryGetComponent ( out PhysicsComponent ? body ) | | body . Fixtures . Count < 1 )
{
continue ;
}
if ( ( body . CollisionLayer & ( int ) CollisionGroup . Impassable ) ! = 0 )
{
impassableEntities . Add ( Tuple . Create ( entity , distance ) ) ;
}
else
{
nonImpassableEntities . Add ( Tuple . Create ( entity , distance ) ) ;
}
}
// The Impassable entities are sorted in descending order
// Entities closer to the epicenter are first
impassableEntities . Sort ( ( x , y ) = > x . Item2 . CompareTo ( y . Item2 ) ) ;
// Impassable entities are handled first. If they are damaged enough, they are destroyed and they may
// be able to spawn a new entity. I.e Wall -> Girder.
// Girder has a tag ExplosivePassable, and the predicate make it so the entities with this tag are ignored
var epicenterMapPos = epicenter . ToMap ( entityManager ) ;
foreach ( var ( entity , distance ) in impassableEntities )
{
if ( ! entity . InRangeUnobstructed ( epicenterMapPos , maxRange , ignoreInsideBlocker : true , predicate : IgnoreExplosivePassable ) )
{
continue ;
}
exAct . HandleExplosion ( epicenter , entity , CalculateSeverity ( distance , devastationRange , heaveyRange ) ) ;
}
// Impassable entities were handled first so NonImpassable entities have a bigger chance to get hit. As now
// there are probably more ExplosivePassable entities around
foreach ( var ( entity , distance ) in nonImpassableEntities )
{
if ( ! entity . InRangeUnobstructed ( epicenterMapPos , maxRange , ignoreInsideBlocker : true , predicate : IgnoreExplosivePassable ) )
{
continue ;
}
exAct . HandleExplosion ( epicenter , entity , CalculateSeverity ( distance , devastationRange , heaveyRange ) ) ;
}
}
/// <summary>
/// Damage tiles inside the range. The type of tile can change depending on a discrete
/// damage bracket [light, heavy, devastation], the distance from the epicenter and
/// a probabilty bracket [<see cref="LightBreakChance"/>, <see cref="HeavyBreakChance"/>, 1.0].
/// </summary>
///
private static void DamageTilesInRange ( EntityCoordinates epicenter ,
GridId gridId ,
Box2 boundingBox ,
float devastationRange ,
float heaveyRange ,
float maxRange )
{
var mapManager = IoCManager . Resolve < IMapManager > ( ) ;
if ( ! mapManager . TryGetGrid ( gridId , out var mapGrid ) )
{
return ;
}
var entityManager = IoCManager . Resolve < IEntityManager > ( ) ;
if ( ! entityManager . TryGetEntity ( mapGrid . GridEntityId , out var grid ) )
{
return ;
}
var robustRandom = IoCManager . Resolve < IRobustRandom > ( ) ;
var tileDefinitionManager = IoCManager . Resolve < ITileDefinitionManager > ( ) ;
var tilesInGridAndCircle = mapGrid . GetTilesIntersecting ( boundingBox ) ;
var epicenterMapPos = epicenter . ToMap ( entityManager ) ;
foreach ( var tile in tilesInGridAndCircle )
{
var tileLoc = mapGrid . GridTileToLocal ( tile . GridIndices ) ;
if ( ! tileLoc . TryDistance ( entityManager , epicenter , out var distance ) | | distance > maxRange )
{
continue ;
}
if ( tile . IsBlockedTurf ( false ) )
{
continue ;
}
if ( ! tileLoc . ToMap ( entityManager ) . InRangeUnobstructed ( epicenterMapPos , maxRange , ignoreInsideBlocker : false , predicate : IgnoreExplosivePassable ) )
{
continue ;
}
var tileDef = ( ContentTileDefinition ) tileDefinitionManager [ tile . Tile . TypeId ] ;
var baseTurfs = tileDef . BaseTurfs ;
if ( baseTurfs . Count = = 0 )
{
continue ;
}
var zeroTile = new Robust . Shared . Map . Tile ( tileDefinitionManager [ baseTurfs [ 0 ] ] . TileId ) ;
var previousTile = new Robust . Shared . Map . Tile ( tileDefinitionManager [ baseTurfs [ ^ 1 ] ] . TileId ) ;
var severity = CalculateSeverity ( distance , devastationRange , heaveyRange ) ;
switch ( severity )
{
case ExplosionSeverity . Light :
if ( ! previousTile . IsEmpty & & robustRandom . Prob ( LightBreakChance ) )
{
mapGrid . SetTile ( tileLoc , previousTile ) ;
}
break ;
case ExplosionSeverity . Heavy :
if ( ! previousTile . IsEmpty & & robustRandom . Prob ( HeavyBreakChance ) )
{
mapGrid . SetTile ( tileLoc , previousTile ) ;
}
break ;
case ExplosionSeverity . Destruction :
mapGrid . SetTile ( tileLoc , zeroTile ) ;
break ;
}
}
}
private static void CameraShakeInRange ( EntityCoordinates epicenter , float maxRange )
{
var playerManager = IoCManager . Resolve < IPlayerManager > ( ) ;
var players = playerManager . GetPlayersInRange ( epicenter , ( int ) Math . Ceiling ( maxRange ) ) ;
foreach ( var player in players )
{
if ( player . AttachedEntity = = null | | ! player . AttachedEntity . TryGetComponent ( out CameraRecoilComponent ? recoil ) )
@@ -231,10 +83,8 @@ namespace Content.Server.Explosion
continue ;
}
var entityManager = IoCManager . Resolve < IEntityManager > ( ) ;
var playerPos = player . AttachedEntity . Transform . WorldPosition ;
var delta = epicenter . ToMapPos ( e ntityManager) - playerPos ;
var delta = epicenter . ToMapPos ( E ntityManager) - playerPos ;
//Change if zero. Will result in a NaN later breaking camera shake if not changed
if ( delta . EqualsApprox ( ( 0.0f , 0.0f ) ) )
@@ -250,83 +100,250 @@ namespace Content.Server.Explosion
}
}
private static void FlashInRange ( EntityCoordinates epicenter , float flashrange )
/// <summary>
/// Damage entities inside the range. The damage depends on a discrete
/// damage bracket [light, heavy, devastation] and the distance from the epicenter
/// </summary>
/// <returns>
/// A dictionary of coordinates relative to the parents of every grid of entities that survived the explosion,
/// have an airtight component and are currently blocking air. Like a wall.
/// </returns>
private void DamageEntitiesInRange (
EntityCoordinates epicenter ,
Box2 boundingBox ,
float devastationRange ,
float heavyRange ,
float maxRange ,
MapId mapId )
{
if ( flashrange > 0 )
var entitiesInRange = _entityLookup . GetEntitiesInRange ( mapId , boundingBox , 0 ) . ToList ( ) ;
var impassableEntities = new List < ( IEntity , float ) > ( ) ;
var nonImpassableEntities = new List < ( IEntity , float ) > ( ) ;
// TODO: Given this seems to rely on physics it should just query directly like everything else.
// The entities are paired with their distance to the epicenter
// and splitted into two lists based on if they are Impassable or not
foreach ( var entity in entitiesInRange )
{
var entitySystemManager = IoCManager . Resolve < IEntitySystemManag er> () ;
var time = IoCManager . Resolve < IGameTiming > ( ) . CurTime ;
if ( entity . Deleted | | entity . IsInContain er( ) )
{
continue ;
}
if ( ! entity . Transform . Coordinates . TryDistance ( EntityManager , epicenter , out var distance ) | |
distance > maxRange )
{
continue ;
}
if ( ! entity . TryGetComponent ( out PhysicsComponent ? body ) | | body . Fixtures . Count < 1 )
{
continue ;
}
if ( ( body . CollisionLayer & ( int ) CollisionGroup . Impassable ) ! = 0 )
{
impassableEntities . Add ( ( entity , distance ) ) ;
}
else
{
nonImpassableEntities . Add ( ( entity , distance ) ) ;
}
}
// The Impassable entities are sorted in descending order
// Entities closer to the epicenter are first
impassableEntities . Sort ( ( x , y ) = > x . Item2 . CompareTo ( y . Item2 ) ) ;
// Impassable entities are handled first. If they are damaged enough, they are destroyed and they may
// be able to spawn a new entity. I.e Wall -> Girder.
// Girder has a tag ExplosivePassable, and the predicate make it so the entities with this tag are ignored
var epicenterMapPos = epicenter . ToMap ( EntityManager ) ;
foreach ( var ( entity , distance ) in impassableEntities )
{
if ( ! entity . InRangeUnobstructed ( epicenterMapPos , maxRange , ignoreInsideBlocker : true , predicate : IgnoreExplosivePassable ) )
{
continue ;
}
_acts . HandleExplosion ( epicenter , entity . Uid , CalculateSeverity ( distance , devastationRange , heavyRange ) ) ;
}
// Impassable entities were handled first so NonImpassable entities have a bigger chance to get hit. As now
// there are probably more ExplosivePassable entities around
foreach ( var ( entity , distance ) in nonImpassableEntities )
{
if ( ! entity . InRangeUnobstructed ( epicenterMapPos , maxRange , ignoreInsideBlocker : true , predicate : IgnoreExplosivePassable ) )
{
continue ;
}
_acts . HandleExplosion ( epicenter , entity . Uid , CalculateSeverity ( distance , devastationRange , heavyRange ) ) ;
}
}
/// <summary>
/// Damage tiles inside the range. The type of tile can change depending on a discrete
/// damage bracket [light, heavy, devastation], the distance from the epicenter and
/// a probability bracket [<see cref="LightBreakChance"/>, <see cref="HeavyBreakChance"/>, 1.0].
/// </summary>
///
private void DamageTilesInRange ( EntityCoordinates epicenter ,
GridId gridId ,
Box2 boundingBox ,
float devastationRange ,
float heaveyRange ,
float maxRange )
{
if ( ! _maps . TryGetGrid ( gridId , out var mapGrid ) )
{
return ;
}
if ( ! EntityManager . EntityExists ( mapGrid . GridEntityId ) )
{
return ;
}
var tilesInGridAndCircle = mapGrid . GetTilesIntersecting ( boundingBox ) ;
var epicenterMapPos = epicenter . ToMap ( EntityManager ) ;
foreach ( var tile in tilesInGridAndCircle )
{
var tileLoc = mapGrid . GridTileToLocal ( tile . GridIndices ) ;
if ( ! tileLoc . TryDistance ( EntityManager , epicenter , out var distance ) | |
distance > maxRange )
{
continue ;
}
if ( tile . IsBlockedTurf ( false ) )
{
continue ;
}
if ( ! tileLoc . ToMap ( EntityManager ) . InRangeUnobstructed ( epicenterMapPos , maxRange , ignoreInsideBlocker : false , predicate : IgnoreExplosivePassable ) )
{
continue ;
}
var tileDef = ( ContentTileDefinition ) _tiles [ tile . Tile . TypeId ] ;
var baseTurfs = tileDef . BaseTurfs ;
if ( baseTurfs . Count = = 0 )
{
continue ;
}
var zeroTile = new Tile ( _tiles [ baseTurfs [ 0 ] ] . TileId ) ;
var previousTile = new Tile ( _tiles [ baseTurfs [ ^ 1 ] ] . TileId ) ;
var severity = CalculateSeverity ( distance , devastationRange , heaveyRange ) ;
switch ( severity )
{
case ExplosionSeverity . Light :
if ( ! previousTile . IsEmpty & & _random . Prob ( LightBreakChance ) )
{
mapGrid . SetTile ( tileLoc , previousTile ) ;
}
break ;
case ExplosionSeverity . Heavy :
if ( ! previousTile . IsEmpty & & _random . Prob ( HeavyBreakChance ) )
{
mapGrid . SetTile ( tileLoc , previousTile ) ;
}
break ;
case ExplosionSeverity . Destruction :
mapGrid . SetTile ( tileLoc , zeroTile ) ;
break ;
}
}
}
private void FlashInRange ( EntityCoordinates epicenter , float flashRange )
{
if ( flashRange > 0 )
{
var time = _timing . CurTime ;
var message = new EffectSystemMessage
{
EffectSprite = "Effects/explosion.rsi" ,
RsiState = "explosionfast" ,
Born = time ,
DeathTime = time + TimeSpan . FromSeconds ( 5 ) ,
Size = new Vector2 ( flashr ange / 2 , flashr ange / 2 ) ,
Size = new Vector2 ( flashR ange / 2 , flashR ange / 2 ) ,
Coordinates = epicenter ,
Rotation = 0f ,
ColorDelta = new Vector4 ( 0 , 0 , 0 , - 1500f ) ,
Color = Vector4 . Multiply ( new Vector4 ( 255 , 255 , 255 , 750 ) , 0.5f ) ,
Shaded = false
} ;
entitySystemManager . GetEntitySystem < EffectSystem > ( ) . CreateParticle ( message ) ;
_effects . CreateParticle ( message ) ;
}
}
// TODO: remove this shit
public static void SpawnExplosion ( this EntityUid uid , int devastationRange = 0 , int heavyImpactRange = 0 ,
int lightImpactRange = 0 , int flashRange = 0 , IEntityManager ? entityManager = null )
public void SpawnExplosion (
EntityUid entity ,
int devastationRange = 0 ,
int heavyImpactRange = 0 ,
int lightImpactRange = 0 ,
int flashRange = 0 ,
ExplosiveComponent ? explosive = null ,
TransformComponent ? transform = null )
{
entityManager ? ? = IoCManager . Resolve < IE ntityManager > ( ) ;
SpawnExplosion ( entityManager . GetEntity ( uid ) , devastationRange , heavyImpactRange , lightImpactRange , flashRange ) ;
}
public static void SpawnExplosion ( this IEntity entity , int devastationRange = 0 , int heavyImpactRange = 0 ,
int lightImpactRange = 0 , int flashRange = 0 )
{
// TODO: Need to refactor this stufferino
// If you want to directly set off the explosive
if ( ! entity . Deleted & & entity . TryGetComponent ( out ExplosiveComponent ? explosive ) & & ! explosive . Exploding )
if ( ! Resolve ( e ntity, ref transform ) )
{
EntitySystem . Get < TriggerSystem > ( ) . Explode ( entity . Uid , explosive ) ;
return ;
}
Resolve ( entity , ref explosive , false ) ;
if ( explosive is { Exploding : false } )
{
_triggers . Explode ( entity , explosive ) ;
}
else
{
while ( e ntity. TryGetContainer ( out var cont ) )
while ( E ntityManager . TryGetComponent ( entity , out ContainerManagerComponent ? container ) )
{
entity = cont . Owner ;
entity = container . OwnerUid ;
}
var epicenter = entity . Transform . Coordinates ;
if ( ! EntityManager . TryGetComponent ( entity , out transform ) )
{
return ;
}
var epicenter = transform . Coordinates ;
SpawnExplosion ( epicenter , devastationRange , heavyImpactRange , lightImpactRange , flashRange ) ;
}
}
public static void SpawnExplosion ( EntityCoordinates epicenter , int devastationRange = 0 ,
int heavyImpactRange = 0 , int lightImpactRange = 0 , int flashRange = 0 )
public void SpawnExplosion (
EntityCoordinates epicenter ,
int devastationRange = 0 ,
int heavyImpactRange = 0 ,
int lightImpactRange = 0 ,
int flashRange = 0 )
{
var mapId = epicenter . GetMapId ( IoCManager . Resolve < I EntityManager> ( ) );
var mapId = epicenter . GetMapId ( EntityManager ) ;
if ( mapId = = MapId . Nullspace )
{
return ;
}
var maxRange = MathHelper . Max ( devastationRange , heavyImpactRange , lightImpactRange , 0 ) ;
var entityManager = IoCManager . Resolve < IEntityManager > ( ) ;
var mapManager = IoCManager . Resolve < IMapManager > ( ) ;
var epicenterMapPos = epicenter . ToMapPos ( entityManager ) ;
var epicenterMapPos = epicenter . ToMapPos ( EntityManager ) ;
var boundingBox = new Box2 ( epicenterMapPos - new Vector2 ( maxRange , maxRange ) ,
epicenterMapPos + new Vector2 ( maxRange , maxRange ) ) ;
SoundSystem . Play ( Filter . Broadcast ( ) , _e xplosionSound. GetSound ( ) , epicenter ) ;
SoundSystem . Play ( Filter . Broadcast ( ) , E xplosionSound. GetSound ( ) , epicenter ) ;
DamageEntitiesInRange ( epicenter , boundingBox , devastationRange , heavyImpactRange , maxRange , mapId ) ;
var mapGridsNear = mapManager . FindGridsIntersecting ( mapId , boundingBox ) ;
var mapGridsNear = _ maps . FindGridsIntersecting ( mapId , boundingBox ) ;
foreach ( var gridId in mapGridsNear )
{