Fixes to explosionhelper (#2819)

* Revert "Make handheld explosives affect tiles (#2806)"

This reverts commit 005e142949.

* Fixes tiles being destroyed under walls by an explosion

* Extra imports removed

* Handles explosion in space and different grids

This handle explosions across different grids, and tiles are still protected if there is an entity airtight and currently blocking air on top of them that survived the explosion.

* Some bug fixes

- The way tiles were being protected was silly.
- Big explosions cause a lot of objects to trigger multiple events and at the same time they are destroyed.
- Explosions spawning inside containers like closets work now.

* Range bug fixes

* Explosive

The explosion works even if the entity exploding is inside multiple 'layers' of containers like.
bomb -> survival box -> tool box -> closet

* Explosions are different now

Explosion can't jump over walls now.
Explosions work like rays now, if an explosion breaks a wall it can scatter inside the room.
If entities are behind impassable entities that survive the blast  they are left unscathed.

* Little fix

* Remove the extra lookup of tiles

* Another small change

* Restore the second lookup

I thought this was extra, but this protects the tile under it if there is an Impassable entity on top. None wants anchored girders on top of lattice/space

* Changing order of conditions

IsBlockedTurf is cheaper to run than InRangeUnobstructed.

* Yep
This commit is contained in:
Daniel Castro Razo
2021-01-02 12:03:10 -06:00
committed by GitHub
parent cd9e5a590b
commit cc4669244d
7 changed files with 281 additions and 104 deletions

View File

@@ -1,20 +1,33 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.Components.Explosion;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Utility;
using Microsoft.Extensions.Logging;
using Robust.Server.GameObjects.EntitySystems; using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.EntitySystemMessages; using Robust.Shared.GameObjects.EntitySystemMessages;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Random; using Robust.Shared.Interfaces.Random;
using Robust.Shared.Interfaces.Timing; using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Explosions namespace Content.Server.Explosions
{ {
@@ -26,62 +39,162 @@ namespace Content.Server.Explosions
/// </summary> /// </summary>
private static readonly Vector2 EpicenterDistance = (0.1f, 0.1f); private static readonly Vector2 EpicenterDistance = (0.1f, 0.1f);
public static void SpawnExplosion(this EntityCoordinates coords, int devastationRange, int heavyImpactRange, int lightImpactRange, int flashRange) /// <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 bool IgnoreExplosivePassable(IEntity e) => (e.GetComponent<IPhysicsComponent>().CollisionLayer & (int) CollisionGroup.ExplosivePassable) != 0;
private static ExplosionSeverity CalculateSeverity(float distance, float devastationRange, float heaveyRange)
{ {
var tileDefinitionManager = IoCManager.Resolve<ITileDefinitionManager>();
var serverEntityManager = IoCManager.Resolve<IServerEntityManager>();
var entitySystemManager = IoCManager.Resolve<IEntitySystemManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var robustRandom = IoCManager.Resolve<IRobustRandom>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var maxRange = MathHelper.Max(devastationRange, heavyImpactRange, lightImpactRange, 0f);
//Entity damage calculation
var entitiesAll = serverEntityManager.GetEntitiesInRange(coords, maxRange).ToList();
foreach (var entity in entitiesAll)
{
if (entity.Deleted)
continue;
if (!entity.Transform.IsMapTransform)
continue;
if (!entity.Transform.Coordinates.TryDistance(entityManager, coords, out var distance))
{
continue;
}
ExplosionSeverity severity;
if (distance < devastationRange) if (distance < devastationRange)
{ {
severity = ExplosionSeverity.Destruction; return ExplosionSeverity.Destruction;
} }
else if (distance < heavyImpactRange) else if (distance < heaveyRange)
{ {
severity = ExplosionSeverity.Heavy; return ExplosionSeverity.Heavy;
}
else if (distance < lightImpactRange)
{
severity = ExplosionSeverity.Light;
} }
else else
{ {
continue; return ExplosionSeverity.Light;
} }
var exAct = entitySystemManager.GetEntitySystem<ActSystem>();
//exAct.HandleExplosion(Owner, entity, severity);
exAct.HandleExplosion(coords, entity, severity);
} }
//Tile damage calculation mockup /// <summary>
//TODO: make it into some sort of actual damage component or whatever the boys think is appropriate /// Damage entities inside the range. The damage depends on a discrete
if (mapManager.TryGetGrid(coords.GetGridId(entityManager), out var mapGrid)) /// 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)
{ {
var circle = new Circle(coords.ToMapPos(entityManager), maxRange); var entityManager = IoCManager.Resolve<IEntityManager>();
var tiles = mapGrid?.GetTilesIntersecting(circle); var serverEntityManager = IoCManager.Resolve<IServerEntityManager>();
foreach (var tile in tiles) var entitySystemManager = IoCManager.Resolve<IEntitySystemManager>();
var exAct = entitySystemManager.GetEntitySystem<ActSystem>();
var entitiesInRange = serverEntityManager.GetEntitiesInRange(mapId, boundingBox, 0).ToList();
var impassableEntities = new List<Tuple<IEntity, float>>();
var nonImpassableEntities = new List<Tuple<IEntity, float>>();
// 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.Transform.IsMapTransform)
{
continue;
}
if (!entity.Transform.Coordinates.TryDistance(entityManager, epicenter, out var distance) || distance > maxRange)
{
continue;
}
if (!entity.TryGetComponent(out IPhysicsComponent body) || body.PhysicsShapes.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 layer ExplosivePassable, and the predicate make it so the entities with this layer 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); 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 tileDef = (ContentTileDefinition) tileDefinitionManager[tile.Tile.TypeId];
var baseTurfs = tileDef.BaseTurfs; var baseTurfs = tileDef.BaseTurfs;
if (baseTurfs.Count == 0) if (baseTurfs.Count == 0)
@@ -89,66 +202,48 @@ namespace Content.Server.Explosions
continue; continue;
} }
if (!tileLoc.TryDistance(entityManager, coords, out var distance))
{
continue;
}
var zeroTile = new Tile(tileDefinitionManager[baseTurfs[0]].TileId); var zeroTile = new Tile(tileDefinitionManager[baseTurfs[0]].TileId);
var previousTile = new Tile(tileDefinitionManager[baseTurfs[^1]].TileId); var previousTile = new Tile(tileDefinitionManager[baseTurfs[^1]].TileId);
switch (distance) var severity = CalculateSeverity(distance, devastationRange, heaveyRange);
switch (severity)
{ {
case var d when d < devastationRange: 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); mapGrid.SetTile(tileLoc, zeroTile);
break; break;
case var d when d < heavyImpactRange
&& !previousTile.IsEmpty
&& robustRandom.Prob(0.8f):
mapGrid.SetTile(tileLoc, previousTile);
break;
case var d when d < lightImpactRange
&& !previousTile.IsEmpty
&& robustRandom.Prob(0.5f):
mapGrid.SetTile(tileLoc, previousTile);
break;
} }
} }
} }
//Effects and sounds private static void CameraShakeInRange(EntityCoordinates epicenter, float maxRange)
var time = IoCManager.Resolve<IGameTiming>().CurTime;
var message = new EffectSystemMessage
{ {
EffectSprite = "Effects/explosion.rsi",
RsiState = "explosionfast",
Born = time,
DeathTime = time + TimeSpan.FromSeconds(5),
Size = new Vector2(flashRange / 2, flashRange / 2),
Coordinates = coords,
//Rotated from east facing
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);
entitySystemManager.GetEntitySystem<AudioSystem>().PlayAtCoords("/Audio/Effects/explosion.ogg", coords);
// Knock back cameras of all players in the area.
var playerManager = IoCManager.Resolve<IPlayerManager>(); var playerManager = IoCManager.Resolve<IPlayerManager>();
foreach (var player in playerManager.GetAllPlayers()) var players = playerManager.GetPlayersInRange(epicenter, (int) Math.Ceiling(maxRange));
foreach (var player in players)
{ {
if (player.AttachedEntity == null if (player.AttachedEntity == null || !player.AttachedEntity.TryGetComponent(out CameraRecoilComponent recoil))
|| player.AttachedEntity.Transform.MapID != mapGrid.ParentMapId
|| !player.AttachedEntity.TryGetComponent(out CameraRecoilComponent recoil))
{ {
continue; continue;
} }
var entityManager = IoCManager.Resolve<IEntityManager>();
var playerPos = player.AttachedEntity.Transform.WorldPosition; var playerPos = player.AttachedEntity.Transform.WorldPosition;
var delta = coords.ToMapPos(entityManager) - playerPos; var delta = epicenter.ToMapPos(entityManager) - playerPos;
//Change if zero. Will result in a NaN later breaking camera shake if not changed //Change if zero. Will result in a NaN later breaking camera shake if not changed
if (delta.EqualsApprox((0.0f, 0.0f))) if (delta.EqualsApprox((0.0f, 0.0f)))
delta = EpicenterDistance; delta = EpicenterDistance;
@@ -163,10 +258,75 @@ namespace Content.Server.Explosions
} }
} }
public static void SpawnExplosion(this IEntity entity, int devastationRange, int heavyImpactRange, private static void FlashInRange(EntityCoordinates epicenter, float flashrange)
int lightImpactRange, int flashRange)
{ {
entity.Transform.Coordinates.SpawnExplosion(devastationRange, heavyImpactRange, lightImpactRange, flashRange); if (flashrange > 0)
{
var entitySystemManager = IoCManager.Resolve<IEntitySystemManager>();
var time = IoCManager.Resolve<IGameTiming>().CurTime;
var message = new EffectSystemMessage
{
EffectSprite = "Effects/explosion.rsi",
RsiState = "explosionfast",
Born = time,
DeathTime = time + TimeSpan.FromSeconds(5),
Size = new Vector2(flashrange / 2, flashrange / 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);
}
}
private static void Detonate(IEntity source, int devastationRange, int heavyImpactRange, int lightImpactRange, int flashRange)
{
var mapId = source.Transform.MapID;
if (mapId == MapId.Nullspace)
{
return;
}
var maxRange = MathHelper.Max(devastationRange, heavyImpactRange, lightImpactRange, 0);
while(source.TryGetContainer(out var cont))
{
source = cont.Owner;
}
var epicenter = source.Transform.Coordinates;
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var epicenterMapPos = epicenter.ToMapPos(entityManager);
var boundingBox = new Box2(epicenterMapPos - new Vector2(maxRange, maxRange), epicenterMapPos + new Vector2(maxRange, maxRange));
DamageEntitiesInRange(epicenter, boundingBox, devastationRange, heavyImpactRange, maxRange, mapId);
var mapGridsNear = mapManager.FindGridsIntersecting(mapId, boundingBox);
foreach (var gridId in mapGridsNear)
{
DamageTilesInRange(epicenter, gridId.Index, boundingBox, devastationRange, heavyImpactRange, maxRange);
}
CameraShakeInRange(epicenter, maxRange);
FlashInRange(epicenter, flashRange);
}
public static void SpawnExplosion(this IEntity entity, int devastationRange = 0, int heavyImpactRange = 0, int lightImpactRange = 0, int flashRange = 0)
{
// If you want to directly set off the explosive
if (!entity.Deleted && entity.TryGetComponent(out ExplosiveComponent explosive) && !explosive.Exploding)
{
explosive.Explosion();
}
else
{
Detonate(entity, devastationRange, heavyImpactRange, lightImpactRange, flashRange);
}
} }
} }
} }

View File

@@ -1,4 +1,4 @@
using Content.Server.Explosions; using Content.Server.Explosions;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -16,7 +16,7 @@ namespace Content.Server.GameObjects.Components.Explosion
public int LightImpactRange = 0; public int LightImpactRange = 0;
public int FlashRange = 0; public int FlashRange = 0;
private bool _beingExploded = false; public bool Exploding { get; private set; } = false;
public override void ExposeData(ObjectSerializer serializer) public override void ExposeData(ObjectSerializer serializer)
{ {
@@ -30,15 +30,18 @@ namespace Content.Server.GameObjects.Components.Explosion
public bool Explosion() public bool Explosion()
{ {
//Prevent adjacent explosives from infinitely blowing each other up. if (Exploding)
if (_beingExploded) return true; {
_beingExploded = true; return false;
}
else
{
Exploding = true;
Owner.SpawnExplosion(DevastationRange, HeavyImpactRange, LightImpactRange, FlashRange); Owner.SpawnExplosion(DevastationRange, HeavyImpactRange, LightImpactRange, FlashRange);
Owner.Delete(); Owner.Delete();
return true; return true;
} }
}
bool ITimerTrigger.Trigger(TimerTriggerEventArgs eventArgs) bool ITimerTrigger.Trigger(TimerTriggerEventArgs eventArgs)
{ {

View File

@@ -1,4 +1,4 @@
#nullable enable #nullable enable
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -470,7 +470,8 @@ namespace Content.Server.GameObjects.Components.Items.Storage
return; return;
} }
foreach (var entity in Contents.ContainedEntities) var containedEntities = Contents.ContainedEntities.ToList();
foreach (var entity in containedEntities)
{ {
var exActs = entity.GetAllComponents<IExAct>().ToArray(); var exActs = entity.GetAllComponents<IExAct>().ToArray();
foreach (var exAct in exActs) foreach (var exAct in exActs)

View File

@@ -34,7 +34,7 @@ namespace Content.Server.GameObjects.Components.Projectiles
void ICollideBehavior.CollideWith(IEntity entity) void ICollideBehavior.CollideWith(IEntity entity)
{ {
if (!_shouldCollide) return; if (!_shouldCollide || entity.Deleted) return;
if (entity.TryGetComponent(out PhysicsComponent collid)) if (entity.TryGetComponent(out PhysicsComponent collid))
{ {
if (!collid.Hard) // ignore non hard if (!collid.Hard) // ignore non hard

View File

@@ -1,4 +1,4 @@
using System; using System;
using Content.Server.GameObjects.Components.Projectiles; using Content.Server.GameObjects.Components.Projectiles;
using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
@@ -43,10 +43,21 @@ namespace Content.Server.Throw
/// </param> /// </param>
public static void Throw(this IEntity thrownEnt, float throwForce, EntityCoordinates targetLoc, EntityCoordinates sourceLoc, bool spread = false, IEntity throwSourceEnt = null) public static void Throw(this IEntity thrownEnt, float throwForce, EntityCoordinates targetLoc, EntityCoordinates sourceLoc, bool spread = false, IEntity throwSourceEnt = null)
{ {
if (thrownEnt.Deleted)
{
return;
}
if (!thrownEnt.TryGetComponent(out IPhysicsComponent colComp)) if (!thrownEnt.TryGetComponent(out IPhysicsComponent colComp))
return; return;
var entityManager = IoCManager.Resolve<IEntityManager>(); var entityManager = IoCManager.Resolve<IEntityManager>();
var direction_vector = targetLoc.ToMapPos(entityManager) - sourceLoc.ToMapPos(entityManager);
if (direction_vector.Length == 0)
{
return;
}
colComp.CanCollide = true; colComp.CanCollide = true;
// I can now collide with player, so that i can do damage. // I can now collide with player, so that i can do damage.
@@ -61,8 +72,8 @@ namespace Content.Server.Throw
colComp.PhysicsShapes[0].CollisionMask |= (int) CollisionGroup.ThrownItem; colComp.PhysicsShapes[0].CollisionMask |= (int) CollisionGroup.ThrownItem;
colComp.Status = BodyStatus.InAir; colComp.Status = BodyStatus.InAir;
} }
var angle = new Angle(targetLoc.ToMapPos(entityManager) - sourceLoc.ToMapPos(entityManager));
var angle = new Angle(direction_vector);
if (spread) if (spread)
{ {
var spreadRandom = IoCManager.Resolve<IRobustRandom>(); var spreadRandom = IoCManager.Resolve<IRobustRandom>();

View File

@@ -1,4 +1,4 @@
using System; using System;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
@@ -23,6 +23,7 @@ namespace Content.Shared.Physics
GhostImpassable = 1 << 6, // 64 Things impassible by ghosts/observers, ie blessed tiles or forcefields GhostImpassable = 1 << 6, // 64 Things impassible by ghosts/observers, ie blessed tiles or forcefields
Underplating = 1 << 7, // 128 Things that are under plating Underplating = 1 << 7, // 128 Things that are under plating
Passable = 1 << 8, // 256 Things that are passable Passable = 1 << 8, // 256 Things that are passable
ExplosivePassable = 1 << 9, // 512 Things that let the pressure of a explosion through
MapGrid = MapGridHelpers.CollisionGroup, // Map grids, like shuttles. This is the actual grid itself, not the walls or other entities connected to the grid. MapGrid = MapGridHelpers.CollisionGroup, // Map grids, like shuttles. This is the actual grid itself, not the walls or other entities connected to the grid.
MobMask = Impassable | MobImpassable | VaultImpassable | SmallImpassable, MobMask = Impassable | MobImpassable | VaultImpassable | SmallImpassable,

View File

@@ -24,6 +24,7 @@
- MobImpassable - MobImpassable
- VaultImpassable - VaultImpassable
- SmallImpassable - SmallImpassable
- ExplosivePassable
- type: Pullable - type: Pullable
- type: Damageable - type: Damageable
resistances: metallicResistances resistances: metallicResistances