Explosion refactor TEST MERG (#6995)
* Explosions * fix yaml typo and prevent silly UI inputs * oop Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Destructible;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Explosion;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
public sealed partial class ExplosionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DestructibleSystem _destructibleSystem = default!;
|
||||
|
||||
// The explosion intensity required to break an entity depends on the explosion type. So it is stored in a
|
||||
// Dictionary<string, float>
|
||||
//
|
||||
// Hence, each tile has a tuple (Dictionary<string, float>, AtmosDirection). This specifies what directions are
|
||||
// blocked, and how intense a given explosion type needs to be in order to destroy ALL airtight entities on that
|
||||
// tile. This is the TileData struct.
|
||||
//
|
||||
// We then need this data for every tile on a grid. So this mess of a variable maps the Grid ID and Vector2i grid
|
||||
// indices to this tile-data struct.
|
||||
private Dictionary<GridId, Dictionary<Vector2i, TileData>> _airtightMap = new();
|
||||
|
||||
public void UpdateAirtightMap(GridId gridId, Vector2i tile)
|
||||
{
|
||||
if (_mapManager.TryGetGrid(gridId, out var grid))
|
||||
UpdateAirtightMap(grid, tile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the map of explosion blockers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Gets a list of all airtight entities on a tile. Assembles a <see cref="AtmosDirection"/> that specifies
|
||||
/// what directions are blocked, along with the largest explosion tolerance. Note that as we only keep track
|
||||
/// of the largest tolerance, this means that the explosion map will actually be inaccurate if you have
|
||||
/// something like a normal and a reinforced windoor on the same tile. But given that this is a pretty rare
|
||||
/// occurrence, I am fine with this.
|
||||
/// </remarks>
|
||||
public void UpdateAirtightMap(IMapGrid grid, Vector2i tile)
|
||||
{
|
||||
Dictionary<string, float> tolerance = new();
|
||||
var blockedDirections = AtmosDirection.Invalid;
|
||||
|
||||
if (!_airtightMap.ContainsKey(grid.Index))
|
||||
_airtightMap[grid.Index] = new();
|
||||
|
||||
foreach (var uid in grid.GetAnchoredEntities(tile))
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(uid, out AirtightComponent? airtight) || !airtight.AirBlocked)
|
||||
continue;
|
||||
|
||||
blockedDirections |= airtight.AirBlockedDirection;
|
||||
foreach (var (type, value) in GetExplosionTolerance(uid))
|
||||
{
|
||||
if (!tolerance.TryAdd(type, value))
|
||||
tolerance[type] = Math.Max(tolerance[type], value);
|
||||
}
|
||||
}
|
||||
|
||||
if (blockedDirections != AtmosDirection.Invalid)
|
||||
_airtightMap[grid.Index][tile] = new(tolerance, blockedDirections);
|
||||
else
|
||||
_airtightMap[grid.Index].Remove(tile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On receiving damage, re-evaluate how much explosion damage is needed to destroy an airtight entity.
|
||||
/// </summary>
|
||||
private void OnAirtightDamaged(EntityUid uid, AirtightComponent airtight, DamageChangedEvent args)
|
||||
{
|
||||
// do we need to update our explosion blocking map?
|
||||
if (!airtight.AirBlocked)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(uid, out TransformComponent transform) || !transform.Anchored)
|
||||
return;
|
||||
|
||||
if (!_mapManager.TryGetGrid(transform.GridID, out var grid))
|
||||
return;
|
||||
|
||||
UpdateAirtightMap(grid, grid.CoordinatesToTile(transform.Coordinates));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a dictionary that specifies how intense a given explosion type needs to be in order to destroy an entity.
|
||||
/// </summary>
|
||||
public Dictionary<string, float> GetExplosionTolerance(EntityUid uid)
|
||||
{
|
||||
// How much total damage is needed to destroy this entity? This also includes "break" behaviors. This ASSUMES
|
||||
// that this will result in a non-airtight entity.Entities that ONLY break via construction graph node changes
|
||||
// are currently effectively "invincible" as far as this is concerned. This really should be done more rigorously.
|
||||
var totalDamageTarget = _destructibleSystem.DestroyedAt(uid);
|
||||
|
||||
Dictionary<string, float> explosionTolerance = new();
|
||||
|
||||
if (totalDamageTarget == FixedPoint2.MaxValue || !TryComp(uid, out DamageableComponent? damageable))
|
||||
return explosionTolerance;
|
||||
|
||||
// What multiple of each explosion type damage set will result in the damage exceeding the required amount? This
|
||||
// does not support entities dynamically changing explosive resistances (e.g. via clothing). But these probably
|
||||
// shouldn't be airtight structures anyways....
|
||||
|
||||
foreach (var explosionType in _prototypeManager.EnumeratePrototypes<ExplosionPrototype>())
|
||||
{
|
||||
// evaluate the damage that this damage type would do to this entity
|
||||
var damagePerIntensity = FixedPoint2.Zero;
|
||||
foreach (var (type, value) in explosionType.DamagePerIntensity.DamageDict)
|
||||
{
|
||||
if (!damageable.Damage.DamageDict.ContainsKey(type))
|
||||
continue;
|
||||
|
||||
var ev = new GetExplosionResistanceEvent(explosionType.ID);
|
||||
RaiseLocalEvent(uid, ev, false);
|
||||
|
||||
damagePerIntensity += value * Math.Clamp(0, 1 - ev.Resistance, 1);
|
||||
}
|
||||
|
||||
explosionTolerance[explosionType.ID] = (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity);
|
||||
}
|
||||
|
||||
return explosionTolerance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data struct that describes the explosion-blocking airtight entities on a tile.
|
||||
/// </summary>
|
||||
internal struct TileData
|
||||
{
|
||||
public TileData(Dictionary<string, float> explosionTolerance, AtmosDirection blockedDirections)
|
||||
{
|
||||
ExplosionTolerance = explosionTolerance;
|
||||
BlockedDirections = blockedDirections;
|
||||
}
|
||||
|
||||
public Dictionary<string, float> ExplosionTolerance;
|
||||
public AtmosDirection BlockedDirections = AtmosDirection.Invalid;
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
// This partial part of the explosion system has all of the functions used to facilitate explosions moving across grids.
|
||||
// A good portion of it is focused around keeping track of what tile-indices on a grid correspond to tiles that border
|
||||
// space. AFAIK no other system currently needs to track these "edge-tiles". If they do, this should probably be a
|
||||
// property of the grid itself?
|
||||
public sealed partial class ExplosionSystem : EntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Set of tiles of each grid that are directly adjacent to space, along with the directions that face space.
|
||||
/// </summary>
|
||||
private Dictionary<GridId, Dictionary<Vector2i, AtmosDirection>> _gridEdges = new();
|
||||
|
||||
/// <summary>
|
||||
/// Set of tiles of each grid that are diagonally adjacent to space
|
||||
/// </summary>
|
||||
private Dictionary<GridId, HashSet<Vector2i>> _diagGridEdges = new();
|
||||
|
||||
/// <summary>
|
||||
/// On grid startup, prepare a map of grid edges.
|
||||
/// </summary>
|
||||
private void OnGridStartup(GridStartupEvent ev)
|
||||
{
|
||||
if (!_mapManager.TryGetGrid(ev.GridId, out var grid))
|
||||
return;
|
||||
|
||||
Dictionary<Vector2i, AtmosDirection> edges = new();
|
||||
HashSet<Vector2i> diagEdges = new();
|
||||
_gridEdges[ev.GridId] = edges;
|
||||
_diagGridEdges[ev.GridId] = diagEdges;
|
||||
|
||||
foreach (var tileRef in grid.GetAllTiles())
|
||||
{
|
||||
if (tileRef.Tile.IsEmpty)
|
||||
continue;
|
||||
|
||||
if (IsEdge(grid, tileRef.GridIndices, out var dir))
|
||||
edges.Add(tileRef.GridIndices, dir);
|
||||
else if (IsDiagonalEdge(grid, tileRef.GridIndices))
|
||||
diagEdges.Add(tileRef.GridIndices);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGridRemoved(GridRemovalEvent ev)
|
||||
{
|
||||
_airtightMap.Remove(ev.GridId);
|
||||
_gridEdges.Remove(ev.GridId);
|
||||
_diagGridEdges.Remove(ev.GridId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Take our map of grid edges, where each is defined in their own grid's reference frame, and map those
|
||||
/// edges all onto one grids reference frame.
|
||||
/// </summary>
|
||||
public (Dictionary<Vector2i, GridBlockData>, ushort) TransformGridEdges(MapId targetMap, GridId? referenceGrid, List<GridId> localGrids)
|
||||
{
|
||||
Dictionary<Vector2i, GridBlockData> transformedEdges = new();
|
||||
|
||||
var targetMatrix = Matrix3.Identity;
|
||||
Angle targetAngle = new();
|
||||
ushort tileSize = DefaultTileSize;
|
||||
|
||||
// if the explosion is centered on some grid (and not just space), get the transforms.
|
||||
if (referenceGrid != null)
|
||||
{
|
||||
var targetGrid = _mapManager.GetGrid(referenceGrid.Value);
|
||||
var xform = Transform(targetGrid.GridEntityId);
|
||||
targetAngle = xform.WorldRotation;
|
||||
targetMatrix = xform.InvWorldMatrix;
|
||||
tileSize = targetGrid.TileSize;
|
||||
}
|
||||
|
||||
var offsetMatrix = Matrix3.Identity;
|
||||
offsetMatrix.R0C2 = tileSize / 2;
|
||||
offsetMatrix.R1C2 = tileSize / 2;
|
||||
|
||||
// here we will get a triple nested for loop:
|
||||
// foreach other grid
|
||||
// foreach edge tile in that grid
|
||||
// foreach tile in our grid that touches that tile (vast majority of the time: 1 tile, but could be up to 4)
|
||||
|
||||
HashSet<Vector2i> transformedTiles = new();
|
||||
foreach (var gridToTransform in localGrids)
|
||||
{
|
||||
// we treat the target grid separately
|
||||
if (gridToTransform == referenceGrid)
|
||||
continue;
|
||||
|
||||
if (!_gridEdges.TryGetValue(gridToTransform, out var edges))
|
||||
continue;
|
||||
|
||||
if (!_mapManager.TryGetGrid(gridToTransform, out var grid) ||
|
||||
grid.ParentMapId != targetMap)
|
||||
continue;
|
||||
|
||||
if (grid.TileSize != tileSize)
|
||||
{
|
||||
Logger.Error($"Explosions do not support grids with different grid sizes. GridIds: {gridToTransform} and {referenceGrid}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(grid.GridEntityId);
|
||||
var matrix = offsetMatrix * xform.WorldMatrix * targetMatrix;
|
||||
var angle = xform.WorldRotation - targetAngle;
|
||||
|
||||
var (x, y) = angle.RotateVec((tileSize / 4, tileSize / 4));
|
||||
|
||||
foreach (var (tile, dir) in edges)
|
||||
{
|
||||
var center = matrix.Transform(tile);
|
||||
|
||||
// this tile might touch several other tiles, or maybe just one tile. Here we use a Vector2i HashSet to
|
||||
// remove duplicates.
|
||||
transformedTiles.Clear();
|
||||
transformedTiles.Add(new((int) MathF.Floor(center.X + x), (int) MathF.Floor(center.Y + y))); // initial direction
|
||||
transformedTiles.Add(new((int) MathF.Floor(center.X - y), (int) MathF.Floor(center.Y + x))); // rotated 90 degrees
|
||||
transformedTiles.Add(new((int) MathF.Floor(center.X - x), (int) MathF.Floor(center.Y - y))); // rotated 180 degrees
|
||||
transformedTiles.Add(new((int) MathF.Floor(center.X + y), (int) MathF.Floor(center.Y - x))); // rotated 270 degrees
|
||||
|
||||
foreach (var newIndices in transformedTiles)
|
||||
{
|
||||
if (!transformedEdges.TryGetValue(newIndices, out var data))
|
||||
{
|
||||
data = new();
|
||||
transformedEdges[newIndices] = data;
|
||||
}
|
||||
data.BlockingGridEdges.Add(new(tile, gridToTransform, center, angle, tileSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next we transform any diagonal edges.
|
||||
Vector2i newIndex;
|
||||
foreach (var gridToTransform in localGrids)
|
||||
{
|
||||
// we treat the target grid separately
|
||||
if (gridToTransform == referenceGrid)
|
||||
continue;
|
||||
|
||||
if (!_diagGridEdges.TryGetValue(gridToTransform, out var diagEdges))
|
||||
continue;
|
||||
|
||||
if (!_mapManager.TryGetGrid(gridToTransform, out var grid) ||
|
||||
grid.ParentMapId != targetMap)
|
||||
continue;
|
||||
|
||||
if (grid.TileSize != tileSize)
|
||||
{
|
||||
Logger.Error($"Explosions do not support grids with different grid sizes. GridIds: {gridToTransform} and {referenceGrid}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(grid.GridEntityId);
|
||||
var matrix = offsetMatrix * xform.WorldMatrix * targetMatrix;
|
||||
var angle = xform.WorldRotation - targetAngle;
|
||||
|
||||
foreach (var tile in diagEdges)
|
||||
{
|
||||
var center = matrix.Transform(tile);
|
||||
newIndex = new((int) MathF.Floor(center.X), (int) MathF.Floor(center.Y));
|
||||
if (!transformedEdges.TryGetValue(newIndex, out var data))
|
||||
{
|
||||
data = new();
|
||||
transformedEdges[newIndex] = data;
|
||||
}
|
||||
|
||||
// explosions are not allowed to propagate diagonally ONTO grids. so we just use defaults for some fields.
|
||||
data.BlockingGridEdges.Add(new(default, null, center, angle, tileSize));
|
||||
}
|
||||
}
|
||||
|
||||
if (referenceGrid == null)
|
||||
return (transformedEdges, tileSize);
|
||||
|
||||
// finally, we also include the blocking tiles from the reference grid.
|
||||
|
||||
if (_gridEdges.TryGetValue(referenceGrid.Value, out var localEdges))
|
||||
{
|
||||
foreach (var (tile, _) in localEdges)
|
||||
{
|
||||
// grids cannot overlap, so tile should NEVER be an existing entry.
|
||||
var data = new GridBlockData();
|
||||
transformedEdges[tile] = data;
|
||||
|
||||
data.UnblockedDirections = AtmosDirection.Invalid; // all directions are blocked automatically.
|
||||
data.BlockingGridEdges.Add(new(tile, referenceGrid.Value, ((Vector2) tile + 0.5f) * tileSize, 0, tileSize));
|
||||
}
|
||||
}
|
||||
|
||||
if (_diagGridEdges.TryGetValue(referenceGrid.Value, out var localDiagEdges))
|
||||
{
|
||||
foreach (var tile in localDiagEdges)
|
||||
{
|
||||
|
||||
// grids cannot overlap, so tile should NEVER be an existing entry.
|
||||
var data = new GridBlockData();
|
||||
transformedEdges[tile] = data;
|
||||
|
||||
data.UnblockedDirections = AtmosDirection.Invalid; // all directions are blocked automatically.
|
||||
data.BlockingGridEdges.Add(new(default, null, ((Vector2) tile + 0.5f) * tileSize, 0, tileSize));
|
||||
}
|
||||
}
|
||||
|
||||
return (transformedEdges, tileSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given an grid-edge blocking map, check if the blockers are allowed to propagate to each other through gaps in grids.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// After grid edges were transformed into the reference frame of some other grid, this function figures out
|
||||
/// which of those edges are actually blocking explosion propagation.
|
||||
/// </remarks>
|
||||
public void GetUnblockedDirections(Dictionary<Vector2i, GridBlockData> transformedEdges, ushort tileSize)
|
||||
{
|
||||
foreach (var (tile, data) in transformedEdges)
|
||||
{
|
||||
if (data.UnblockedDirections == AtmosDirection.Invalid)
|
||||
continue; // already all blocked.
|
||||
|
||||
var tileCenter = ((Vector2) tile + 0.5f) * tileSize;
|
||||
foreach (var edge in data.BlockingGridEdges)
|
||||
{
|
||||
// if a blocking edge contains the center of the tile, block all directions
|
||||
if (edge.Box.Contains(tileCenter))
|
||||
{
|
||||
data.UnblockedDirections = AtmosDirection.Invalid;
|
||||
break;
|
||||
}
|
||||
|
||||
// check north
|
||||
if (edge.Box.Contains(tileCenter + (0, tileSize / 2)))
|
||||
data.UnblockedDirections &= ~AtmosDirection.North;
|
||||
|
||||
// check south
|
||||
if (edge.Box.Contains(tileCenter + (0, -tileSize / 2)))
|
||||
data.UnblockedDirections &= ~AtmosDirection.South;
|
||||
|
||||
// check east
|
||||
if (edge.Box.Contains(tileCenter + (tileSize / 2, 0)))
|
||||
data.UnblockedDirections &= ~AtmosDirection.East;
|
||||
|
||||
// check west
|
||||
if (edge.Box.Contains(tileCenter + (-tileSize / 2, 0)))
|
||||
data.UnblockedDirections &= ~AtmosDirection.West;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a tile is updated, we might need to update the grid edge maps.
|
||||
/// </summary>
|
||||
private void OnTileChanged(object? sender, TileChangedEventArgs e)
|
||||
{
|
||||
// only need to update the grid-edge map if the tile changed from space to not-space.
|
||||
if (!e.NewTile.Tile.IsEmpty && !e.OldTile.IsEmpty)
|
||||
return;
|
||||
|
||||
var tileRef = e.NewTile;
|
||||
|
||||
if (!_mapManager.TryGetGrid(tileRef.GridIndex, out var grid))
|
||||
return;
|
||||
|
||||
if (!_gridEdges.TryGetValue(tileRef.GridIndex, out var edges))
|
||||
{
|
||||
edges = new();
|
||||
_gridEdges[tileRef.GridIndex] = edges;
|
||||
}
|
||||
|
||||
if (!_diagGridEdges.TryGetValue(tileRef.GridIndex, out var diagEdges))
|
||||
{
|
||||
diagEdges = new();
|
||||
_diagGridEdges[tileRef.GridIndex] = diagEdges;
|
||||
}
|
||||
|
||||
if (tileRef.Tile.IsEmpty)
|
||||
{
|
||||
// if the tile is empty, it cannot itself be an edge tile.
|
||||
edges.Remove(tileRef.GridIndices);
|
||||
diagEdges.Remove(tileRef.GridIndices);
|
||||
|
||||
// add any valid neighbours to the list of edge-tiles
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection) (1 << i);
|
||||
|
||||
var neighbourIndex = tileRef.GridIndices.Offset(direction);
|
||||
|
||||
if (grid.TryGetTileRef(neighbourIndex, out var neighbourTile) && !neighbourTile.Tile.IsEmpty)
|
||||
{
|
||||
edges[neighbourIndex] = edges.GetValueOrDefault(neighbourIndex) | direction.GetOpposite();
|
||||
diagEdges.Remove(neighbourIndex);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var diagNeighbourIndex in GetDiagonalNeighbors(tileRef.GridIndices))
|
||||
{
|
||||
if (edges.ContainsKey(diagNeighbourIndex))
|
||||
continue;
|
||||
|
||||
if (grid.TryGetTileRef(diagNeighbourIndex, out var neighbourIndex) && !neighbourIndex.Tile.IsEmpty)
|
||||
diagEdges.Add(diagNeighbourIndex);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// the tile is not empty space, but was previously. So update directly adjacent neighbours, which may no longer
|
||||
// be edge tiles.
|
||||
AtmosDirection spaceDir;
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection) (1 << i);
|
||||
var neighbourIndex = tileRef.GridIndices.Offset(direction);
|
||||
|
||||
if (edges.TryGetValue(neighbourIndex, out spaceDir))
|
||||
{
|
||||
spaceDir = spaceDir & ~direction.GetOpposite();
|
||||
if (spaceDir != AtmosDirection.Invalid)
|
||||
edges[neighbourIndex] = spaceDir;
|
||||
else
|
||||
{
|
||||
// no longer a direct edge ...
|
||||
edges.Remove(neighbourIndex);
|
||||
|
||||
// ... but it could now be a diagonal edge
|
||||
if (IsDiagonalEdge(grid, neighbourIndex, tileRef.GridIndices))
|
||||
diagEdges.Add(neighbourIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// and again for diagonal neighbours
|
||||
foreach (var neighborIndex in GetDiagonalNeighbors(tileRef.GridIndices))
|
||||
{
|
||||
if (diagEdges.Contains(neighborIndex) && !IsDiagonalEdge(grid, neighborIndex, tileRef.GridIndices))
|
||||
diagEdges.Remove(neighborIndex);
|
||||
}
|
||||
|
||||
// finally check if the new tile is itself an edge tile
|
||||
if (IsEdge(grid, tileRef.GridIndices, out spaceDir))
|
||||
edges.Add(tileRef.GridIndices, spaceDir);
|
||||
else if (IsDiagonalEdge(grid, tileRef.GridIndices))
|
||||
diagEdges.Add(tileRef.GridIndices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a tile is on the edge of a grid (i.e., whether it borders space).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Optionally ignore a specific Vector2i. Used by <see cref="OnTileChanged"/> when we already know that a
|
||||
/// given tile is not space. This avoids unnecessary TryGetTileRef calls.
|
||||
/// </remarks>
|
||||
private bool IsEdge(IMapGrid grid, Vector2i index, out AtmosDirection spaceDirections)
|
||||
{
|
||||
spaceDirections = AtmosDirection.Invalid;
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection) (1 << i);
|
||||
|
||||
if (!grid.TryGetTileRef(index.Offset(direction), out var neighborTile) || neighborTile.Tile.IsEmpty)
|
||||
spaceDirections |= direction;
|
||||
}
|
||||
|
||||
return spaceDirections != AtmosDirection.Invalid;
|
||||
}
|
||||
|
||||
private bool IsDiagonalEdge(IMapGrid grid, Vector2i index, Vector2i? ignore = null)
|
||||
{
|
||||
foreach (var neighbourIndex in GetDiagonalNeighbors(index))
|
||||
{
|
||||
if (neighbourIndex == ignore)
|
||||
continue;
|
||||
|
||||
if (!grid.TryGetTileRef(neighbourIndex, out var neighborTile) || neighborTile.Tile.IsEmpty)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate over diagonally adjacent tiles.
|
||||
/// </summary>
|
||||
internal static IEnumerable<Vector2i> GetDiagonalNeighbors(Vector2i pos)
|
||||
{
|
||||
yield return pos + (1, 1);
|
||||
yield return pos + (-1, -1);
|
||||
yield return pos + (1, -1);
|
||||
yield return pos + (-1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public struct GridEdgeData : IEquatable<GridEdgeData>
|
||||
{
|
||||
public Vector2i Tile;
|
||||
public GridId? Grid;
|
||||
public Box2Rotated Box;
|
||||
|
||||
public GridEdgeData(Vector2i tile, GridId? grid, Vector2 center, Angle angle, float size)
|
||||
{
|
||||
Tile = tile;
|
||||
Grid = grid;
|
||||
Box = new(Box2.CenteredAround(center, (size, size)), angle, center);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(GridEdgeData other)
|
||||
{
|
||||
return Tile.Equals(other.Tile) && Grid.Equals(other.Grid);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (Tile.GetHashCode() * 397) ^ Grid.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record GridBlockData
|
||||
{
|
||||
/// <summary>
|
||||
/// What directions of this tile are not blocked by some other grid?
|
||||
/// </summary>
|
||||
public AtmosDirection UnblockedDirections = AtmosDirection.All;
|
||||
|
||||
/// <summary>
|
||||
/// Hashset contains information about the edge-tiles, which belong to some other grid(s), that are blocking
|
||||
/// this tile.
|
||||
/// </summary>
|
||||
public HashSet<GridEdgeData> BlockingGridEdges = new();
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Explosion.Components;
|
||||
using Content.Server.Throwing;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Explosion;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
public sealed partial class ExplosionSystem : EntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to identify explosions when communicating with the client. Might be needed if more than one explosion is spawned in a single tick.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Overflowing back to 0 should cause no issue, as long as you don't have more than 256 explosions happening in a single tick.
|
||||
/// </remarks>
|
||||
private byte _explosionCounter = 0;
|
||||
// maybe should just use a UID/explosion-entity and a state to convey information?
|
||||
// but then need to ignore PVS? Eeehh this works well enough for now.
|
||||
|
||||
/// <summary>
|
||||
/// Arbitrary definition for when an explosion is large enough to require separating the area/tile-finding and
|
||||
/// the processing into separate ticks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only used when the explosion processing is not limited by time.
|
||||
/// </remarks>
|
||||
public const int NukeArea = 400;
|
||||
|
||||
/// <summary>
|
||||
/// Used to limit explosion processing time. See <see cref="MaxProcessingTime"/>.
|
||||
/// </summary>
|
||||
internal readonly Stopwatch Stopwatch = new();
|
||||
|
||||
/// <summary>
|
||||
/// How many tiles to explode before checking the stopwatch timer
|
||||
/// </summary>
|
||||
internal static int TileCheckIteration = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Queue for delayed processing of explosions. If there is an explosion that covers more than <see
|
||||
/// cref="TilesPerTick"/> tiles, other explosions will actually be delayed slightly. Unless it's a station
|
||||
/// nuke, this delay should never really be noticeable.
|
||||
/// </summary>
|
||||
private Queue<Func<Explosion?>> _explosionQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// The explosion currently being processed.
|
||||
/// </summary>
|
||||
private Explosion? _activeExplosion;
|
||||
|
||||
/// <summary>
|
||||
/// While processing an explosion, the "progress" is sent to clients, so that the explosion fireball effect
|
||||
/// syncs up with the damage. When the tile iteration increments, an update needs to be sent to clients.
|
||||
/// This integer keeps track of the last value sent to clients.
|
||||
/// </summary>
|
||||
private int _previousTileIteration;
|
||||
|
||||
/// <summary>
|
||||
/// Process the explosion queue.
|
||||
/// </summary>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
if (_activeExplosion == null && _explosionQueue.Count == 0)
|
||||
// nothing to do
|
||||
return;
|
||||
|
||||
Stopwatch.Restart();
|
||||
var x = Stopwatch.Elapsed.TotalMilliseconds;
|
||||
|
||||
var availableTime = MaxProcessingTime;
|
||||
|
||||
var tilesRemaining = TilesPerTick;
|
||||
while (tilesRemaining > 0 && MaxProcessingTime > Stopwatch.Elapsed.TotalMilliseconds)
|
||||
{
|
||||
// if there is no active explosion, get a new one to process
|
||||
if (_activeExplosion == null)
|
||||
{
|
||||
// EXPLOSION TODO allow explosion spawning to be interrupted by time limit. In the meantime, ensure that
|
||||
// there is at-least 1ms of time left before creating a new explosion
|
||||
if (MathF.Max(MaxProcessingTime - 1, 0.1f) < Stopwatch.Elapsed.TotalMilliseconds)
|
||||
break;
|
||||
|
||||
if (!_explosionQueue.TryDequeue(out var spawnNextExplosion))
|
||||
break;
|
||||
|
||||
_activeExplosion = spawnNextExplosion();
|
||||
|
||||
// explosion spawning can be null if something somewhere went wrong. (e.g., negative explosion
|
||||
// intensity).
|
||||
if (_activeExplosion == null)
|
||||
continue;
|
||||
|
||||
_explosionCounter++;
|
||||
_previousTileIteration = 0;
|
||||
|
||||
// just a lil nap
|
||||
if (SleepNodeSys)
|
||||
{
|
||||
_nodeGroupSystem.Snoozing = true;
|
||||
// snooze grid-chunk regeneration?
|
||||
// snooze power network (recipients look for new suppliers as wires get destroyed).
|
||||
}
|
||||
|
||||
// if this is a single-tick explosion (i.e., not severely limited by number of tiles per tick or
|
||||
// processing time, we want to process large explosion on a tick separate from the one they were
|
||||
// generated on.
|
||||
if (_activeExplosion.Area > NukeArea
|
||||
&& MaxProcessingTime >= _gameTiming.TickPeriod.TotalMilliseconds)
|
||||
{
|
||||
// start processing next turn.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var processed = _activeExplosion.Proccess(tilesRemaining);
|
||||
tilesRemaining -= processed;
|
||||
|
||||
// has the explosion finished processing?
|
||||
if (_activeExplosion.FinishedProcessing)
|
||||
_activeExplosion = null;
|
||||
}
|
||||
|
||||
Logger.InfoS("Explosion", $"Processed {TilesPerTick - tilesRemaining} tiles in {Stopwatch.Elapsed.TotalMilliseconds}ms");
|
||||
|
||||
// we have finished processing our tiles. Is there still an ongoing explosion?
|
||||
if (_activeExplosion != null)
|
||||
{
|
||||
// update the client explosion overlays. This ensures that the fire-effects sync up with the entities currently being damaged.
|
||||
if (_previousTileIteration == _activeExplosion.CurrentIteration)
|
||||
return;
|
||||
|
||||
_previousTileIteration = _activeExplosion.CurrentIteration;
|
||||
RaiseNetworkEvent(new ExplosionOverlayUpdateEvent(_explosionCounter, _previousTileIteration + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_explosionQueue.Count > 0)
|
||||
return;
|
||||
|
||||
// We have finished processing all explosions. Clear client explosion overlays
|
||||
RaiseNetworkEvent(new ExplosionOverlayUpdateEvent(_explosionCounter, int.MaxValue));
|
||||
|
||||
//wakey wakey
|
||||
_nodeGroupSystem.Snoozing = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an entity is blocking a tile or not. (whether it can prevent the tile from being uprooted
|
||||
/// by an explosion).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used for a variation of <see cref="TurfHelpers.IsBlockedTurf()"/> that makes use of the fact that we have
|
||||
/// already done an entity lookup on a tile, and don't need to do so again.
|
||||
/// </remarks>
|
||||
public bool IsBlockingTurf(EntityUid uid)
|
||||
{
|
||||
if (EntityManager.IsQueuedForDeletion(uid))
|
||||
return false;
|
||||
|
||||
if (!TryComp(uid, out IPhysBody? body))
|
||||
return false;
|
||||
|
||||
return body.CanCollide && body.Hard && (body.CollisionLayer & (int) CollisionGroup.Impassable) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find entities on a grid tile using the EntityLookupComponent and apply explosion effects.
|
||||
/// </summary>
|
||||
/// <returns>True if the underlying tile can be uprooted, false if the tile is blocked by a dense entity</returns>
|
||||
internal bool ExplodeTile(EntityLookupComponent lookup,
|
||||
IMapGrid grid,
|
||||
Vector2i tile,
|
||||
float intensity,
|
||||
float throwForce,
|
||||
DamageSpecifier damage,
|
||||
MapCoordinates epicenter,
|
||||
HashSet<EntityUid> processed,
|
||||
string id)
|
||||
{
|
||||
var gridBox = new Box2(tile * grid.TileSize, (tile + 1) * grid.TileSize);
|
||||
|
||||
// get the entities on a tile. Note that we cannot process them directly, or we get
|
||||
// enumerator-changed-while-enumerating errors.
|
||||
List<EntityUid> list = new();
|
||||
_entityLookup.FastEntitiesIntersecting(lookup, ref gridBox, entity => list.Add(entity));
|
||||
|
||||
// process those entities
|
||||
foreach (var entity in list)
|
||||
{
|
||||
ProcessEntity(entity, epicenter, processed, damage, throwForce, id, false);
|
||||
}
|
||||
|
||||
// process anchored entities
|
||||
var tileBlocked = false;
|
||||
foreach (var entity in grid.GetAnchoredEntities(tile).ToList())
|
||||
{
|
||||
ProcessEntity(entity, epicenter, processed, damage, throwForce, id, true);
|
||||
tileBlocked |= IsBlockingTurf(entity);
|
||||
}
|
||||
|
||||
// Next, we get the intersecting entities AGAIN, but purely for throwing. This way, glass shards spawned from
|
||||
// windows will be flung outwards, and not stay where they spawned. This is however somewhat unnecessary, and a
|
||||
// prime candidate for computational cost-cutting. Alternatively, it would be nice if there was just some sort
|
||||
// of spawned-on-destruction event that could be used to automatically assemble a list of new entities that need
|
||||
// to be thrown.
|
||||
//
|
||||
// All things considered, until entity spawning & destruction is sped up, this isn't all that time consuming.
|
||||
// (unless its a REALLY big explosion)
|
||||
if (throwForce <= 0)
|
||||
return !tileBlocked;
|
||||
|
||||
list.Clear();
|
||||
_entityLookup.FastEntitiesIntersecting(lookup, ref gridBox, entity => list.Add(entity));
|
||||
|
||||
foreach (var e in list)
|
||||
{
|
||||
// Here we only throw, no dealing damage. Containers n such might drop their entities after being destroyed, but
|
||||
// they handle their own damage pass-through.
|
||||
ProcessEntity(e, epicenter, processed, null, throwForce, id, false);
|
||||
}
|
||||
|
||||
return !tileBlocked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="ExplodeTile"/>, but for SPAAAAAAACE.
|
||||
/// </summary>
|
||||
internal void ExplodeSpace(EntityLookupComponent lookup,
|
||||
Matrix3 spaceMatrix,
|
||||
Matrix3 invSpaceMatrix,
|
||||
Vector2i tile,
|
||||
float intensity,
|
||||
float throwForce,
|
||||
DamageSpecifier damage,
|
||||
MapCoordinates epicenter,
|
||||
HashSet<EntityUid> processed,
|
||||
string id)
|
||||
{
|
||||
var gridBox = new Box2(tile * DefaultTileSize, (DefaultTileSize, DefaultTileSize));
|
||||
var worldBox = spaceMatrix.TransformBox(gridBox);
|
||||
List<EntityUid> list = new();
|
||||
|
||||
EntityUidQueryCallback callback = uid =>
|
||||
{
|
||||
if (gridBox.Contains(invSpaceMatrix.Transform(Transform(uid).WorldPosition)))
|
||||
list.Add(uid);
|
||||
};
|
||||
|
||||
_entityLookup.FastEntitiesIntersecting(lookup, ref worldBox, callback);
|
||||
|
||||
foreach (var entity in list)
|
||||
{
|
||||
ProcessEntity(entity, epicenter, processed, damage, throwForce, id, false);
|
||||
}
|
||||
|
||||
if (throwForce <= 0)
|
||||
return;
|
||||
|
||||
list.Clear();
|
||||
_entityLookup.FastEntitiesIntersecting(lookup, ref worldBox, callback);
|
||||
foreach (var entity in list)
|
||||
{
|
||||
ProcessEntity(entity, epicenter, processed, null, throwForce, id, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function actually applies the explosion affects to an entity.
|
||||
/// </summary>
|
||||
private void ProcessEntity(EntityUid uid, MapCoordinates epicenter, HashSet<EntityUid> processed, DamageSpecifier? damage, float throwForce, string id, bool anchored)
|
||||
{
|
||||
// check whether this is a valid target, and whether we have already damaged this entity (can happen with
|
||||
// explosion-throwing).
|
||||
if (!anchored && _containerSystem.IsEntityInContainer(uid) || !processed.Add(uid))
|
||||
return;
|
||||
|
||||
// damage
|
||||
if (damage != null)
|
||||
{
|
||||
var ev = new GetExplosionResistanceEvent(id);
|
||||
RaiseLocalEvent(uid, ev, false);
|
||||
var coeff = Math.Clamp(0, 1 - ev.Resistance, 1);
|
||||
|
||||
if (!MathHelper.CloseTo(0, coeff))
|
||||
_damageableSystem.TryChangeDamage(uid, damage * coeff, ignoreResistances: true);
|
||||
}
|
||||
|
||||
// throw
|
||||
if (!anchored
|
||||
&& throwForce > 0
|
||||
&& !EntityManager.IsQueuedForDeletion(uid)
|
||||
&& HasComp<ExplosionLaunchedComponent>(uid)
|
||||
&& TryComp(uid, out TransformComponent? transform))
|
||||
{
|
||||
uid.TryThrow(transform.WorldPosition - epicenter.Position, throwForce);
|
||||
}
|
||||
|
||||
// TODO EXPLOSION puddle / flammable ignite?
|
||||
|
||||
// TODO EXPLOSION deaf/ear damage? other explosion effects?
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to damage floor tiles. Not to be confused with the function that damages entities intersecting the
|
||||
/// grid tile.
|
||||
/// </summary>
|
||||
public void DamageFloorTile(TileRef tileRef,
|
||||
float intensity,
|
||||
List<(Vector2i GridIndices, Tile Tile)> damagedTiles,
|
||||
ExplosionPrototype type)
|
||||
{
|
||||
var tileDef = _tileDefinitionManager[tileRef.Tile.TypeId];
|
||||
|
||||
while (_robustRandom.Prob(type.TileBreakChance(intensity)))
|
||||
{
|
||||
intensity -= type.TileBreakRerollReduction;
|
||||
|
||||
if (tileDef is not ContentTileDefinition contentTileDef)
|
||||
break;
|
||||
|
||||
// does this have a base-turf that we can break it down to?
|
||||
if (contentTileDef.BaseTurfs.Count == 0)
|
||||
break;
|
||||
|
||||
tileDef = _tileDefinitionManager[contentTileDef.BaseTurfs[^1]];
|
||||
}
|
||||
|
||||
if (tileDef.TileId == tileRef.Tile.TypeId)
|
||||
return;
|
||||
|
||||
damagedTiles.Add((tileRef.GridIndices, new Tile(tileDef.TileId)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a data class that stores information about the area affected by an explosion, for processing by <see
|
||||
/// cref="ExplosionSystem"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is basically the output of <see cref="ExplosionSystem.GetExplosionTiles()"/>, but wrapped in an enumerator
|
||||
/// to iterate over the tiles, along with the ability to keep track of what entities have already been damaged by
|
||||
/// this explosion.
|
||||
/// </remarks>
|
||||
sealed class Explosion
|
||||
{
|
||||
struct ExplosionData
|
||||
{
|
||||
public EntityLookupComponent Lookup;
|
||||
public Dictionary<int, List<Vector2i>> TileLists;
|
||||
public IMapGrid? MapGrid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to avoid applying explosion effects repeatedly to the same entity. Particularly important if the
|
||||
/// explosion throws this entity, as then it will be moving while the explosion is happening.
|
||||
/// </summary>
|
||||
public readonly HashSet<EntityUid> ProcessedEntities = new();
|
||||
|
||||
/// <summary>
|
||||
/// This integer tracks how much of this explosion has been processed.
|
||||
/// </summary>
|
||||
public int CurrentIteration { get; private set; } = 0;
|
||||
|
||||
public readonly ExplosionPrototype ExplosionType;
|
||||
public readonly MapCoordinates Epicenter;
|
||||
private readonly Matrix3 _spaceMatrix;
|
||||
private readonly Matrix3 _invSpaceMatrix;
|
||||
|
||||
private readonly List<ExplosionData> _explosionData = new();
|
||||
private readonly List<float> _tileSetIntensity;
|
||||
|
||||
public bool FinishedProcessing;
|
||||
|
||||
// shitty enumerator implementation
|
||||
private DamageSpecifier _currentDamage = default!;
|
||||
private EntityLookupComponent _currentLookup = default!;
|
||||
private IMapGrid? _currentGrid;
|
||||
private float _currentIntensity;
|
||||
private float _currentThrowForce;
|
||||
private List<Vector2i>.Enumerator _currentEnumerator;
|
||||
private int _currentDataIndex;
|
||||
private Dictionary<IMapGrid, List<(Vector2i, Tile)>> _tileUpdateDict = new();
|
||||
|
||||
public int Area;
|
||||
|
||||
private readonly ExplosionSystem _system;
|
||||
|
||||
public Explosion(ExplosionSystem system,
|
||||
ExplosionPrototype explosionType,
|
||||
SpaceExplosion? spaceData,
|
||||
List<GridExplosion> gridData,
|
||||
List<float> tileSetIntensity,
|
||||
MapCoordinates epicenter,
|
||||
Matrix3 spaceMatrix,
|
||||
int area)
|
||||
{
|
||||
_system = system;
|
||||
ExplosionType = explosionType;
|
||||
_tileSetIntensity = tileSetIntensity;
|
||||
Epicenter = epicenter;
|
||||
Area = area;
|
||||
|
||||
var entityMan = IoCManager.Resolve<IEntityManager>();
|
||||
var mapMan = IoCManager.Resolve<IMapManager>();
|
||||
|
||||
if (spaceData != null)
|
||||
{
|
||||
var mapUid = mapMan.GetMapEntityId(epicenter.MapId);
|
||||
|
||||
_explosionData.Add(new()
|
||||
{
|
||||
TileLists = spaceData.TileLists,
|
||||
Lookup = entityMan.GetComponent<EntityLookupComponent>(mapUid),
|
||||
MapGrid = null
|
||||
});
|
||||
|
||||
_spaceMatrix = spaceMatrix;
|
||||
_invSpaceMatrix = Matrix3.Invert(spaceMatrix);
|
||||
}
|
||||
|
||||
foreach (var grid in gridData)
|
||||
{
|
||||
_explosionData.Add(new()
|
||||
{
|
||||
TileLists = grid.TileLists,
|
||||
Lookup = entityMan.GetComponent<EntityLookupComponent>(grid.Grid.GridEntityId),
|
||||
MapGrid = grid.Grid
|
||||
});
|
||||
}
|
||||
|
||||
TryGetNextTileEnumerator();
|
||||
}
|
||||
|
||||
private bool TryGetNextTileEnumerator()
|
||||
{
|
||||
while (CurrentIteration < _tileSetIntensity.Count)
|
||||
{
|
||||
_currentIntensity = _tileSetIntensity[CurrentIteration];
|
||||
_currentDamage = ExplosionType.DamagePerIntensity * _currentIntensity;
|
||||
_currentThrowForce = Area > _system.ThrowLimit ? 0 : 10 * MathF.Sqrt(_currentIntensity);
|
||||
|
||||
// for each grid/space tile set
|
||||
while (_currentDataIndex < _explosionData.Count)
|
||||
{
|
||||
// try get any tile hash-set corresponding to this intensity
|
||||
var tileSets = _explosionData[_currentDataIndex].TileLists;
|
||||
if (!tileSets.TryGetValue(CurrentIteration, out var tileList))
|
||||
{
|
||||
_currentDataIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
_currentEnumerator = tileList.GetEnumerator();
|
||||
_currentLookup = _explosionData[_currentDataIndex].Lookup;
|
||||
_currentGrid = _explosionData[_currentDataIndex].MapGrid;
|
||||
|
||||
_currentDataIndex++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// this explosion intensity has been fully processed, move to the next one
|
||||
CurrentIteration++;
|
||||
_currentDataIndex = 0;
|
||||
}
|
||||
|
||||
// no more explosion data to process
|
||||
FinishedProcessing = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool MoveNext()
|
||||
{
|
||||
if (FinishedProcessing)
|
||||
return false;
|
||||
|
||||
while (!FinishedProcessing)
|
||||
{
|
||||
if (_currentEnumerator.MoveNext())
|
||||
return true;
|
||||
else
|
||||
TryGetNextTileEnumerator();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public int Proccess(int processingTarget)
|
||||
{
|
||||
// In case the explosion terminated early last tick due to exceeding the allocated processing time, use this
|
||||
// time to update the tiles.
|
||||
SetTiles();
|
||||
|
||||
int processed;
|
||||
for (processed = 0; processed < processingTarget; processed++)
|
||||
{
|
||||
if (processed % ExplosionSystem.TileCheckIteration == 0 &&
|
||||
_system.Stopwatch.Elapsed.TotalMilliseconds > _system.MaxProcessingTime)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (_currentGrid != null &&
|
||||
_currentGrid.TryGetTileRef(_currentEnumerator.Current, out var tileRef) &&
|
||||
!tileRef.Tile.IsEmpty)
|
||||
{
|
||||
if (!_tileUpdateDict.TryGetValue(_currentGrid, out var tileUpdateList))
|
||||
{
|
||||
tileUpdateList = new();
|
||||
_tileUpdateDict[_currentGrid] = tileUpdateList;
|
||||
}
|
||||
|
||||
var canDamageFloor = _system.ExplodeTile(_currentLookup,
|
||||
_currentGrid,
|
||||
_currentEnumerator.Current,
|
||||
_currentIntensity,
|
||||
_currentThrowForce,
|
||||
_currentDamage,
|
||||
Epicenter,
|
||||
ProcessedEntities,
|
||||
ExplosionType.ID);
|
||||
|
||||
if (canDamageFloor)
|
||||
_system.DamageFloorTile(tileRef, _currentIntensity, tileUpdateList, ExplosionType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_system.ExplodeSpace(_currentLookup,
|
||||
_spaceMatrix,
|
||||
_invSpaceMatrix,
|
||||
_currentEnumerator.Current,
|
||||
_currentIntensity,
|
||||
_currentThrowForce,
|
||||
_currentDamage,
|
||||
Epicenter,
|
||||
ProcessedEntities,
|
||||
ExplosionType.ID);
|
||||
}
|
||||
|
||||
if (!MoveNext())
|
||||
break;
|
||||
}
|
||||
|
||||
SetTiles();
|
||||
return processed;
|
||||
}
|
||||
|
||||
private void SetTiles()
|
||||
{
|
||||
if (!_system.IncrementalTileBreaking && !FinishedProcessing)
|
||||
return;
|
||||
|
||||
foreach (var (grid, list) in _tileUpdateDict)
|
||||
{
|
||||
if (list.Count > 0)
|
||||
{
|
||||
grid.SetTiles(list);
|
||||
}
|
||||
}
|
||||
_tileUpdateDict.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Explosion;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
// This partial part of the explosion system has all of the functions used to create the actual explosion map.
|
||||
// I.e, to get the sets of tiles & intensity values that describe an explosion.
|
||||
|
||||
public sealed partial class ExplosionSystem : EntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the main explosion generating function.
|
||||
/// </summary>
|
||||
/// <param name="epicenter">The center of the explosion</param>
|
||||
/// <param name="typeID">The explosion type. this determines the explosion damage</param>
|
||||
/// <param name="totalIntensity">The final sum of the tile intensities. This governs the overall size of the
|
||||
/// explosion</param>
|
||||
/// <param name="slope">How quickly does the intensity decrease when moving away from the epicenter.</param>
|
||||
/// <param name="maxIntensity">The maximum intensity that the explosion can have at any given tile. This
|
||||
/// effectively caps the damage that this explosion can do.</param>
|
||||
/// <returns>A list of tile-sets and a list of intensity values which describe the explosion.</returns>
|
||||
private (int, List<float>, SpaceExplosion?, Dictionary<GridId, GridExplosion>, Matrix3)? GetExplosionTiles(
|
||||
MapCoordinates epicenter,
|
||||
string typeID,
|
||||
float totalIntensity,
|
||||
float slope,
|
||||
float maxIntensity)
|
||||
{
|
||||
if (totalIntensity <= 0 || slope <= 0)
|
||||
return null;
|
||||
|
||||
Vector2i initialTile;
|
||||
GridId? epicentreGrid = null;
|
||||
var (localGrids, referenceGrid) = GetLocalGrids(epicenter, totalIntensity, slope, maxIntensity);
|
||||
|
||||
// get the epicenter tile indices
|
||||
if (_mapManager.TryFindGridAt(epicenter, out var candidateGrid) &&
|
||||
candidateGrid.TryGetTileRef(candidateGrid.WorldToTile(epicenter.Position), out var tileRef) &&
|
||||
!tileRef.Tile.IsEmpty)
|
||||
{
|
||||
epicentreGrid = candidateGrid.Index;
|
||||
initialTile = tileRef.GridIndices;
|
||||
}
|
||||
else if (referenceGrid != null)
|
||||
{
|
||||
// reference grid defines coordinate system that the explosion in space will use
|
||||
initialTile = _mapManager.GetGrid(referenceGrid.Value).WorldToTile(epicenter.Position);
|
||||
}
|
||||
else
|
||||
{
|
||||
// this is a space-based explosion that (should) not touch any grids.
|
||||
initialTile = new Vector2i(
|
||||
(int) Math.Floor(epicenter.Position.X / DefaultTileSize),
|
||||
(int) Math.Floor(epicenter.Position.Y / DefaultTileSize));
|
||||
}
|
||||
|
||||
// Main data for the exploding tiles in space and on various grids
|
||||
Dictionary<GridId, GridExplosion> gridData = new();
|
||||
SpaceExplosion? spaceData = null;
|
||||
|
||||
// The intensity slope is how much the intensity drop over a one-tile distance. The actual algorithm step-size is half of thhat.
|
||||
var stepSize = slope / 2;
|
||||
|
||||
// Hashsets used for when grid-based explosion propagate into space. Basically: used to move data between
|
||||
// `gridData` and `spaceData` in-between neighbor finding iterations.
|
||||
HashSet<Vector2i> spaceJump = new();
|
||||
HashSet<Vector2i> previousSpaceJump;
|
||||
|
||||
// As above, but for space-based explosion propagating from space onto grids.
|
||||
HashSet<GridId> encounteredGrids = new();
|
||||
Dictionary<GridId, HashSet<Vector2i>>? previousGridJump;
|
||||
|
||||
// variables for transforming between grid and space-coordiantes
|
||||
var spaceMatrix = Matrix3.Identity;
|
||||
var spaceAngle = Angle.Zero;
|
||||
if (referenceGrid != null)
|
||||
{
|
||||
var xform = Transform(_mapManager.GetGrid(referenceGrid.Value).GridEntityId);
|
||||
spaceMatrix = xform.WorldMatrix;
|
||||
spaceAngle = xform.WorldRotation;
|
||||
}
|
||||
|
||||
// is the explosion starting on a grid?
|
||||
if (epicentreGrid != null)
|
||||
{
|
||||
// set up the initial `gridData` instance
|
||||
encounteredGrids.Add(epicentreGrid.Value);
|
||||
|
||||
if (!_airtightMap.TryGetValue(epicentreGrid.Value, out var airtightMap))
|
||||
airtightMap = new();
|
||||
|
||||
var initialGridData = new GridExplosion(
|
||||
_mapManager.GetGrid(epicentreGrid.Value),
|
||||
airtightMap,
|
||||
maxIntensity,
|
||||
stepSize,
|
||||
typeID,
|
||||
_gridEdges[epicentreGrid.Value],
|
||||
referenceGrid,
|
||||
spaceMatrix,
|
||||
spaceAngle);
|
||||
|
||||
gridData[epicentreGrid.Value] = initialGridData;
|
||||
|
||||
initialGridData.InitTile(initialTile);
|
||||
}
|
||||
else
|
||||
{
|
||||
// set up the space explosion data
|
||||
spaceData = new SpaceExplosion(this, epicenter.MapId, referenceGrid, localGrids);
|
||||
spaceData.InitTile(initialTile);
|
||||
}
|
||||
|
||||
// Is this even a multi-tile explosion?
|
||||
if (totalIntensity < stepSize)
|
||||
// Bit anticlimactic. All that set up for nothing....
|
||||
return (1, new List<float> { totalIntensity }, spaceData, gridData, spaceMatrix);
|
||||
|
||||
// These variables keep track of the total intensity we have distributed
|
||||
List<int> tilesInIteration = new() { 1 };
|
||||
List<float> iterationIntensity = new() {stepSize};
|
||||
var totalTiles = 0;
|
||||
var remainingIntensity = totalIntensity - stepSize;
|
||||
|
||||
var iteration = 1;
|
||||
var maxIntensityIndex = 0;
|
||||
|
||||
// If an explosion is trapped in an indestructible room, we can end the neighbor finding steps early.
|
||||
// These variables are used to check if we can abort early.
|
||||
float previousIntensity;
|
||||
var intensityUnchangedLastLoop = false;
|
||||
|
||||
// Main flood-fill / neighbor-finding loop
|
||||
while (remainingIntensity > 0 && iteration <= MaxIterations && totalTiles < MaxArea)
|
||||
{
|
||||
previousIntensity = remainingIntensity;
|
||||
|
||||
// First, we increase the intensity of the tiles that were already discovered in previous iterations.
|
||||
for (var i = maxIntensityIndex; i < iteration; i++)
|
||||
{
|
||||
var intensityIncrease = MathF.Min(stepSize, maxIntensity - iterationIntensity[i]);
|
||||
|
||||
if (tilesInIteration[i] * intensityIncrease >= remainingIntensity)
|
||||
{
|
||||
// there is not enough intensity left to distribute. add a fractional amount and break.
|
||||
iterationIntensity[i] += (float) remainingIntensity / tilesInIteration[i];
|
||||
remainingIntensity = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
iterationIntensity[i] += intensityIncrease;
|
||||
remainingIntensity -= tilesInIteration[i] * intensityIncrease;
|
||||
|
||||
// Has this tile-set has reached max intensity? If so, stop iterating over it in future
|
||||
if (intensityIncrease < stepSize)
|
||||
maxIntensityIndex++;
|
||||
}
|
||||
|
||||
if (remainingIntensity <= 0) break;
|
||||
|
||||
// Next, we will add a new iteration of tiles
|
||||
|
||||
// In order to treat "cost" of moving off a grid on the same level as moving onto a grid, both space -> grid and grid -> space have to be delayed by one iteration.
|
||||
previousSpaceJump = spaceJump;
|
||||
previousGridJump = spaceData?.GridJump;
|
||||
spaceJump = new();
|
||||
|
||||
var newTileCount = 0;
|
||||
|
||||
if (previousGridJump != null)
|
||||
encounteredGrids.UnionWith(previousGridJump.Keys);
|
||||
|
||||
foreach (var grid in encounteredGrids)
|
||||
{
|
||||
// is this a new grid, for which we must create a new explosion data set
|
||||
if (!gridData.TryGetValue(grid, out var data))
|
||||
{
|
||||
if (!_airtightMap.TryGetValue(grid, out var airtightMap))
|
||||
airtightMap = new();
|
||||
|
||||
data = new GridExplosion(
|
||||
_mapManager.GetGrid(grid),
|
||||
airtightMap,
|
||||
maxIntensity,
|
||||
stepSize,
|
||||
typeID,
|
||||
_gridEdges[grid],
|
||||
referenceGrid,
|
||||
spaceMatrix,
|
||||
spaceAngle);
|
||||
|
||||
gridData[grid] = data;
|
||||
}
|
||||
|
||||
// get the new neighbours, and populate gridToSpaceTiles in the process.
|
||||
newTileCount += data.AddNewTiles(iteration, previousGridJump?.GetValueOrDefault(grid));
|
||||
spaceJump.UnionWith(data.SpaceJump);
|
||||
}
|
||||
|
||||
// if space-data is null, but some grid-based explosion reached space, we need to initialize it.
|
||||
if (spaceData == null && previousSpaceJump.Count != 0)
|
||||
spaceData = new SpaceExplosion(this, epicenter.MapId, referenceGrid, localGrids);
|
||||
|
||||
// If the explosion has reached space, do that neighbors finding step as well.
|
||||
if (spaceData != null)
|
||||
newTileCount += spaceData.AddNewTiles(iteration, previousSpaceJump);
|
||||
|
||||
// Does adding these tiles bring us above the total target intensity?
|
||||
tilesInIteration.Add(newTileCount);
|
||||
if (newTileCount * stepSize >= remainingIntensity)
|
||||
{
|
||||
iterationIntensity.Add((float) remainingIntensity / newTileCount);
|
||||
break;
|
||||
}
|
||||
|
||||
// add the new tiles and decrement available intensity
|
||||
remainingIntensity -= newTileCount * stepSize;
|
||||
iterationIntensity.Add(stepSize);
|
||||
totalTiles += newTileCount;
|
||||
|
||||
// It is possible that the explosion has some max intensity and is stuck in a container whose walls it
|
||||
// cannot break. if the remaining intensity remains unchanged TWO loops in a row, we know that this is the
|
||||
// case.
|
||||
if (intensityUnchangedLastLoop && remainingIntensity == previousIntensity)
|
||||
break;
|
||||
|
||||
intensityUnchangedLastLoop = remainingIntensity == previousIntensity;
|
||||
iteration += 1;
|
||||
}
|
||||
|
||||
if (totalTiles >= MaxArea)
|
||||
Logger.Info("Whooooo! MAXCAP!");
|
||||
|
||||
// Neighbor finding is done. Perform final clean up and return.
|
||||
foreach (var grid in gridData.Values)
|
||||
{
|
||||
grid.CleanUp();
|
||||
}
|
||||
spaceData?.CleanUp();
|
||||
|
||||
return (totalTiles, iterationIntensity, spaceData, gridData, spaceMatrix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Look for grids in an area and returns them. Also selects a special grid that will be used to determine the
|
||||
/// orientation of an explosion in space.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that even though an explosion may start ON a grid, the explosion in space may still be orientated to
|
||||
/// match a separate grid. This is done so that if you have something like a tiny suicide-bomb shuttle exploding
|
||||
/// near a large station, the explosion will still orient to match the station, not the tiny shuttle.
|
||||
/// </remarks>
|
||||
public (List<GridId>, GridId?) GetLocalGrids(MapCoordinates epicenter, float totalIntensity, float slope, float maxIntensity)
|
||||
{
|
||||
// Get the explosion radius (approx radius if it were in open-space). Note that if the explosion is confined in
|
||||
// some directions but not in others, the actual explosion may reach further than this distance from the
|
||||
// epicenter. Conversely, it might go nowhere near as far.
|
||||
var radius = 0.5f + IntensityToRadius(totalIntensity, slope, maxIntensity);
|
||||
|
||||
// to avoid a silly lookup for silly input numbers, cap the radius to half of the theoretical maximum (lookup area gets doubled later on).
|
||||
radius = Math.Min(radius, MaxIterations / 4);
|
||||
|
||||
GridId? referenceGrid = null;
|
||||
float mass = 0;
|
||||
|
||||
// First attempt to find a grid that is relatively close to the explosion's center. Instead of looking in a
|
||||
// diameter x diameter sized box, use a smaller box with radius sized sides:
|
||||
var box = Box2.CenteredAround(epicenter.Position, (radius, radius));
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(epicenter.MapId, box))
|
||||
{
|
||||
if (TryComp(grid.GridEntityId, out PhysicsComponent? physics) && physics.Mass > mass)
|
||||
{
|
||||
mass = physics.Mass;
|
||||
referenceGrid = grid.Index;
|
||||
}
|
||||
}
|
||||
|
||||
// Next, we use a much larger lookup to determine all grids relevant to the explosion. This is used to determine
|
||||
// what grids should be includes during the grid-edge transformation steps. This means that if a grid is not in
|
||||
// this set, the explosion can never propagate from space onto this grid.
|
||||
|
||||
// As mentioned before, the `diameter` is only indicative, as an explosion that is obstructed (e.g., in a
|
||||
// tunnel) may travel further away from the epicenter. But this should be very rare for space-traversing
|
||||
// explosions. So instead of using the largest possible distance that an explosion could theoretically travel
|
||||
// and using that for the grid look-up, we will just arbitrarily fudge the lookup size to be twice the diameter.
|
||||
|
||||
box = box.Scale(4); // box with width and height of 4*radius.
|
||||
var mapGrids = _mapManager.FindGridsIntersecting(epicenter.MapId, box).ToList();
|
||||
var grids = mapGrids.Select(x => x.Index).ToList();
|
||||
|
||||
if (referenceGrid != null)
|
||||
return (grids, referenceGrid);
|
||||
|
||||
// We still don't have are reference grid. So lets also look in the enlarged region
|
||||
foreach (var grid in mapGrids)
|
||||
{
|
||||
if (TryComp(grid.GridEntityId, out PhysicsComponent? physics) && physics.Mass > mass)
|
||||
{
|
||||
mass = physics.Mass;
|
||||
referenceGrid = grid.Index;
|
||||
}
|
||||
}
|
||||
|
||||
return (grids, referenceGrid);
|
||||
}
|
||||
|
||||
public ExplosionEvent? GenerateExplosionPreview(SpawnExplosionEuiMsg.PreviewRequest request)
|
||||
{
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
var results = GetExplosionTiles(
|
||||
request.Epicenter,
|
||||
request.TypeId,
|
||||
request.TotalIntensity,
|
||||
request.IntensitySlope,
|
||||
request.MaxIntensity);
|
||||
|
||||
if (results == null)
|
||||
return null;
|
||||
|
||||
var (area, iterationIntensity, spaceData, gridData, spaceMatrix) = results.Value;
|
||||
|
||||
Logger.Info($"Generated explosion preview with {area} tiles in {stopwatch.Elapsed.TotalMilliseconds}ms");
|
||||
|
||||
// the explosion event that **would** be sent to all clients, if it were a real explosion.
|
||||
return GetExplosionEvent(request.Epicenter, request.TypeId, spaceMatrix, spaceData, gridData.Values, iterationIntensity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,392 +1,274 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Explosion.Components;
|
||||
using Content.Shared.Acts;
|
||||
using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Shared.Camera;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Sound;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Explosion;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
public sealed partial class ExplosionSystem : EntitySystem
|
||||
{
|
||||
public sealed 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!;
|
||||
|
||||
/// <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);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
base.Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Distance used for camera shake when distance from explosion is (0.0, 0.0).
|
||||
/// Avoids getting NaN values down the line from doing math on (0.0, 0.0).
|
||||
/// </summary>
|
||||
private static readonly Vector2 EpicenterDistance = (0.1f, 0.1f);
|
||||
// handled in ExplosionSystemGridMap.cs
|
||||
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
|
||||
SubscribeLocalEvent<GridStartupEvent>(OnGridStartup);
|
||||
SubscribeLocalEvent<ExplosionResistanceComponent, GetExplosionResistanceEvent>(OnGetResistance);
|
||||
_mapManager.TileChanged += OnTileChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Chance of a tile breaking if the severity is Light and Heavy
|
||||
/// </summary>
|
||||
private const float LightBreakChance = 0.3f;
|
||||
private const float HeavyBreakChance = 0.8f;
|
||||
// handled in ExplosionSystemAirtight.cs
|
||||
SubscribeLocalEvent<AirtightComponent, DamageChangedEvent>(OnAirtightDamaged);
|
||||
SubscribeCvars();
|
||||
}
|
||||
|
||||
// TODO move this to the component
|
||||
private static readonly SoundSpecifier ExplosionSound = new SoundCollectionSpecifier("explosion");
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_mapManager.TileChanged -= OnTileChanged;
|
||||
UnsubscribeCvars();
|
||||
}
|
||||
|
||||
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IMapManager _maps = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ITileDefinitionManager _tiles = default!;
|
||||
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;
|
||||
}
|
||||
|
||||
[Dependency] private readonly ActSystem _acts = default!;
|
||||
[Dependency] private readonly EffectSystem _effects = default!;
|
||||
[Dependency] private readonly TriggerSystem _triggers = default!;
|
||||
[Dependency] private readonly AdminLogSystem _logSystem = default!;
|
||||
[Dependency] private readonly CameraRecoilSystem _cameraRecoil = default!;
|
||||
[Dependency] private readonly TagSystem _tags = default!;
|
||||
/// <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)
|
||||
{
|
||||
// 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;
|
||||
|
||||
private bool IgnoreExplosivePassable(EntityUid e)
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
return _tags.HasTag(e, "ExplosivePassable");
|
||||
// maxIntensity is a non-issue, can use simple inverse formula
|
||||
return MathF.Cbrt(3 * totalIntensity / (slope * MathF.PI));
|
||||
}
|
||||
|
||||
private ExplosionSeverity CalculateSeverity(float distance, float devastationRange, float heavyRange)
|
||||
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 intensity,
|
||||
float slope,
|
||||
float maxTileIntensity)
|
||||
{
|
||||
QueueExplosion(Transform(uid).MapPosition, typeId, intensity, slope, maxTileIntensity);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (totalIntensity <= 0 || slope <= 0)
|
||||
return;
|
||||
|
||||
if (!_prototypeManager.TryIndex<ExplosionPrototype>(typeId, out var type))
|
||||
{
|
||||
if (distance < devastationRange)
|
||||
{
|
||||
return ExplosionSeverity.Destruction;
|
||||
}
|
||||
else if (distance < heavyRange)
|
||||
{
|
||||
return ExplosionSeverity.Heavy;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ExplosionSeverity.Light;
|
||||
}
|
||||
Logger.Error($"Attempted to spawn unknown explosion prototype: {type}");
|
||||
return;
|
||||
}
|
||||
|
||||
private void CameraShakeInRange(EntityCoordinates epicenter, float maxRange)
|
||||
_explosionQueue.Enqueue(() => SpawnExplosion(epicenter, type, totalIntensity,
|
||||
slope, maxTileIntensity));
|
||||
}
|
||||
|
||||
/// <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,
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the shared <see cref="ExplosionEvent"/> using the server-exclusive explosion classes.
|
||||
/// </summary>
|
||||
internal ExplosionEvent GetExplosionEvent(MapCoordinates epicenter, string id, Matrix3 spaceMatrix, SpaceExplosion? spaceData, IEnumerable<GridExplosion> gridData, List<float> iterationIntensity)
|
||||
{
|
||||
var spaceTiles = spaceData?.TileLists;
|
||||
|
||||
Dictionary<GridId, Dictionary<int, List<Vector2i>>> tileLists = new();
|
||||
foreach (var grid in gridData)
|
||||
{
|
||||
var players = Filter.Empty()
|
||||
.AddInRange(epicenter.ToMap(EntityManager), MathF.Ceiling(maxRange))
|
||||
.Recipients;
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
if (player.AttachedEntity is not {Valid: true} playerEntity ||
|
||||
!EntityManager.HasComponent<CameraRecoilComponent>(playerEntity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var playerPos = EntityManager.GetComponent<TransformComponent>(playerEntity).WorldPosition;
|
||||
var delta = epicenter.ToMapPos(EntityManager) - playerPos;
|
||||
|
||||
//Change if zero. Will result in a NaN later breaking camera shake if not changed
|
||||
if (delta.EqualsApprox((0.0f, 0.0f)))
|
||||
delta = EpicenterDistance;
|
||||
|
||||
var distance = delta.LengthSquared;
|
||||
var effect = 10 * (1 / (1 + distance));
|
||||
if (effect > 0.01f)
|
||||
{
|
||||
var kick = -delta.Normalized * effect;
|
||||
_cameraRecoil.KickCamera(player.AttachedEntity.Value, kick);
|
||||
}
|
||||
}
|
||||
tileLists.Add(grid.Grid.Index, grid.TileLists);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
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)
|
||||
{
|
||||
var entitiesInRange = _entityLookup.GetEntitiesInRange(mapId, boundingBox, 0).ToList();
|
||||
if (player.AttachedEntity is not EntityUid uid)
|
||||
continue;
|
||||
|
||||
var impassableEntities = new List<(EntityUid, float)>();
|
||||
var nonImpassableEntities = new List<(EntityUid, float)>();
|
||||
// TODO: Given this seems to rely on physics it should just query directly like everything else.
|
||||
var playerPos = Transform(player.AttachedEntity!.Value).WorldPosition;
|
||||
var delta = epicenter.Position - playerPos;
|
||||
|
||||
// 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 (Deleted(entity) || entity.IsInContainer())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (delta.EqualsApprox(Vector2.Zero))
|
||||
delta = new(0.01f, 0);
|
||||
|
||||
if (!EntityManager.GetComponent<TransformComponent>(entity).Coordinates.TryDistance(EntityManager, epicenter, out var distance) ||
|
||||
distance > maxRange)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(entity, out FixturesComponent? fixturesComp) || fixturesComp.Fixtures.Count < 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(entity, out PhysicsComponent? body))
|
||||
{
|
||||
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 (!_interactionSystem.InRangeUnobstructed(epicenterMapPos, entity, maxRange, predicate: IgnoreExplosivePassable))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_acts.HandleExplosion(epicenter, entity, 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 (!_interactionSystem.InRangeUnobstructed(epicenterMapPos, entity, maxRange, predicate: IgnoreExplosivePassable))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_acts.HandleExplosion(epicenter, entity, 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 (!_interactionSystem.InRangeUnobstructed(tileLoc.ToMap(EntityManager), epicenterMapPos, maxRange, 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(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
|
||||
};
|
||||
|
||||
_effects.CreateParticle(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void SpawnExplosion(
|
||||
EntityUid entity,
|
||||
int devastationRange = 0,
|
||||
int heavyImpactRange = 0,
|
||||
int lightImpactRange = 0,
|
||||
int flashRange = 0,
|
||||
EntityUid? user = null,
|
||||
ExplosiveComponent? explosive = null,
|
||||
TransformComponent? transform = null)
|
||||
{
|
||||
if (!Resolve(entity, ref transform))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Resolve(entity, ref explosive, false);
|
||||
|
||||
if (explosive is { Exploding: false })
|
||||
{
|
||||
_triggers.Explode(entity, explosive, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
while (EntityManager.EntityExists(entity) && entity.TryGetContainer(out var container))
|
||||
{
|
||||
entity = container.Owner;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(entity, out transform))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var epicenter = transform.Coordinates;
|
||||
|
||||
SpawnExplosion(epicenter, devastationRange, heavyImpactRange, lightImpactRange, flashRange, entity, user);
|
||||
}
|
||||
}
|
||||
|
||||
public void SpawnExplosion(
|
||||
EntityCoordinates epicenter,
|
||||
int devastationRange = 0,
|
||||
int heavyImpactRange = 0,
|
||||
int lightImpactRange = 0,
|
||||
int flashRange = 0,
|
||||
EntityUid? entity = null,
|
||||
EntityUid? user = null)
|
||||
{
|
||||
var mapId = epicenter.GetMapId(EntityManager);
|
||||
if (mapId == MapId.Nullspace)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// logging
|
||||
var range = $"{devastationRange}/{heavyImpactRange}/{lightImpactRange}/{flashRange}";
|
||||
if (entity == null || !entity.Value.IsValid())
|
||||
{
|
||||
_logSystem.Add(LogType.Explosion, LogImpact.High, $"Explosion spawned at {epicenter:coordinates} with range {range}");
|
||||
}
|
||||
else if (user == null || !user.Value.IsValid())
|
||||
{
|
||||
_logSystem.Add(LogType.Explosion, LogImpact.High,
|
||||
$"{ToPrettyString(entity.Value):entity} exploded at {epicenter:coordinates} with range {range}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logSystem.Add(LogType.Explosion, LogImpact.High,
|
||||
$"{ToPrettyString(user.Value):user} caused {ToPrettyString(entity.Value):entity} to explode at {epicenter:coordinates} with range {range}");
|
||||
}
|
||||
|
||||
var maxRange = MathHelper.Max(devastationRange, heavyImpactRange, lightImpactRange, 0);
|
||||
var epicenterMapPos = epicenter.ToMapPos(EntityManager);
|
||||
var boundingBox = new Box2(epicenterMapPos - new Vector2(maxRange, maxRange),
|
||||
epicenterMapPos + new Vector2(maxRange, maxRange));
|
||||
|
||||
SoundSystem.Play(Filter.Broadcast(), ExplosionSound.GetSound(), epicenter);
|
||||
DamageEntitiesInRange(epicenter, boundingBox, devastationRange, heavyImpactRange, maxRange, mapId);
|
||||
|
||||
var mapGridsNear = _maps.FindGridsIntersecting(mapId, boundingBox);
|
||||
|
||||
foreach (var gridId in mapGridsNear)
|
||||
{
|
||||
DamageTilesInRange(epicenter, gridId.Index, boundingBox, devastationRange, heavyImpactRange, maxRange);
|
||||
}
|
||||
|
||||
CameraShakeInRange(epicenter, maxRange);
|
||||
FlashInRange(epicenter, flashRange);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using Content.Shared.CCVar;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
public sealed partial class ExplosionSystem : EntitySystem
|
||||
{
|
||||
public int MaxIterations { get; private set; }
|
||||
public int MaxArea { get; private set; }
|
||||
public float MaxProcessingTime { get; private set; }
|
||||
public int TilesPerTick { get; private set; }
|
||||
public int ThrowLimit { get; private set; }
|
||||
public bool SleepNodeSys { get; private set; }
|
||||
public bool IncrementalTileBreaking { get; private set; }
|
||||
|
||||
private void SubscribeCvars()
|
||||
{
|
||||
_cfg.OnValueChanged(CCVars.ExplosionTilesPerTick, SetTilesPerTick, true);
|
||||
_cfg.OnValueChanged(CCVars.ExplosionThrowLimit, SetThrowLimit, true);
|
||||
_cfg.OnValueChanged(CCVars.ExplosionSleepNodeSys, SetSleepNodeSys, true);
|
||||
_cfg.OnValueChanged(CCVars.ExplosionMaxArea, SetMaxArea, true);
|
||||
_cfg.OnValueChanged(CCVars.ExplosionMaxIterations, SetMaxIterations, true);
|
||||
_cfg.OnValueChanged(CCVars.ExplosionMaxProcessingTime, SetMaxProcessingTime, true);
|
||||
_cfg.OnValueChanged(CCVars.ExplosionIncrementalTileBreaking, SetIncrementalTileBreaking, true);
|
||||
}
|
||||
|
||||
private void UnsubscribeCvars()
|
||||
{
|
||||
_cfg.UnsubValueChanged(CCVars.ExplosionTilesPerTick, SetTilesPerTick);
|
||||
_cfg.UnsubValueChanged(CCVars.ExplosionThrowLimit, SetThrowLimit);
|
||||
_cfg.UnsubValueChanged(CCVars.ExplosionSleepNodeSys, SetSleepNodeSys);
|
||||
_cfg.UnsubValueChanged(CCVars.ExplosionMaxArea, SetMaxArea);
|
||||
_cfg.UnsubValueChanged(CCVars.ExplosionMaxIterations, SetMaxIterations);
|
||||
_cfg.UnsubValueChanged(CCVars.ExplosionMaxProcessingTime, SetMaxProcessingTime);
|
||||
_cfg.UnsubValueChanged(CCVars.ExplosionIncrementalTileBreaking, SetIncrementalTileBreaking);
|
||||
}
|
||||
|
||||
private void SetTilesPerTick(int value) => TilesPerTick = value;
|
||||
private void SetThrowLimit(int value) => ThrowLimit = value;
|
||||
private void SetSleepNodeSys(bool value) => SleepNodeSys = value;
|
||||
private void SetMaxArea(int value) => MaxArea = value;
|
||||
private void SetMaxIterations(int value) => MaxIterations = value;
|
||||
private void SetMaxProcessingTime(float value) => MaxProcessingTime = value;
|
||||
private void SetIncrementalTileBreaking(bool value) => IncrementalTileBreaking = value;
|
||||
}
|
||||
311
Content.Server/Explosion/EntitySystems/GridExplosion.cs
Normal file
311
Content.Server/Explosion/EntitySystems/GridExplosion.cs
Normal file
@@ -0,0 +1,311 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
internal sealed class GridExplosion : TileExplosion
|
||||
{
|
||||
public IMapGrid Grid;
|
||||
private bool _needToTransform = false;
|
||||
|
||||
private Matrix3 _matrix = Matrix3.Identity;
|
||||
private Vector2 _offset;
|
||||
|
||||
private HashSet<Vector2i> _processedSpaceTiles = new();
|
||||
|
||||
// Tiles which neighbor an exploding tile, but have not yet had the explosion spread to them due to an
|
||||
// airtight entity on the exploding tile that prevents the explosion from spreading in that direction. These
|
||||
// will be added as a neighbor after some delay, once the explosion on that tile is sufficiently strong to
|
||||
// destroy the airtight entity.
|
||||
private Dictionary<int, List<(Vector2i, AtmosDirection)>> _delayedNeighbors = new();
|
||||
|
||||
private Dictionary<Vector2i, TileData> _airtightMap;
|
||||
|
||||
private float _maxIntensity;
|
||||
private float _intensityStepSize;
|
||||
private string _typeID;
|
||||
|
||||
/// <summary>
|
||||
/// Tiles on this grid that are not actually on this grid.... uhh ... yeah.... look its faster than checking
|
||||
/// atmos directions every iteration.
|
||||
/// </summary>
|
||||
private HashSet<Vector2i> _spaceTiles = new();
|
||||
|
||||
public HashSet<Vector2i> SpaceJump = new();
|
||||
|
||||
private Dictionary<Vector2i, AtmosDirection> _edgeTiles;
|
||||
|
||||
public GridExplosion(
|
||||
IMapGrid grid,
|
||||
Dictionary<Vector2i, TileData> airtightMap,
|
||||
float maxIntensity,
|
||||
float intensityStepSize,
|
||||
string typeID,
|
||||
Dictionary<Vector2i, AtmosDirection> edgeTiles,
|
||||
GridId? referenceGrid,
|
||||
Matrix3 spaceMatrix,
|
||||
Angle spaceAngle)
|
||||
{
|
||||
Grid = grid;
|
||||
_airtightMap = airtightMap;
|
||||
_maxIntensity = maxIntensity;
|
||||
_intensityStepSize = intensityStepSize;
|
||||
_typeID = typeID;
|
||||
_edgeTiles = edgeTiles;
|
||||
|
||||
// initialise SpaceTiles
|
||||
foreach (var (tile, dir) in _edgeTiles)
|
||||
{
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection) (1 << i);
|
||||
if (dir.IsFlagSet(direction))
|
||||
_spaceTiles.Add(tile.Offset(direction));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO EXPLOSIONS fix this shit.
|
||||
foreach (var tile in _edgeTiles.Keys)
|
||||
{
|
||||
foreach (var diagTile in ExplosionSystem.GetDiagonalNeighbors(tile))
|
||||
{
|
||||
if (_spaceTiles.Contains(diagTile))
|
||||
continue;
|
||||
|
||||
if (!Grid.TryGetTileRef(diagTile, out var tileRef) || tileRef.Tile.IsEmpty)
|
||||
_spaceTiles.Add(diagTile);
|
||||
}
|
||||
}
|
||||
|
||||
if (referenceGrid == Grid.Index)
|
||||
return;
|
||||
|
||||
_needToTransform = true;
|
||||
var transform = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Grid.GridEntityId);
|
||||
var size = (float) Grid.TileSize;
|
||||
|
||||
_matrix.R0C2 = size / 2;
|
||||
_matrix.R1C2 = size / 2;
|
||||
_matrix *= transform.WorldMatrix * Matrix3.Invert(spaceMatrix);
|
||||
var relativeAngle = transform.WorldRotation - spaceAngle;
|
||||
_offset = relativeAngle.RotateVec((size / 4, size / 4));
|
||||
}
|
||||
|
||||
public int AddNewTiles(int iteration, HashSet<Vector2i>? gridJump)
|
||||
{
|
||||
SpaceJump = new();
|
||||
NewTiles = new();
|
||||
NewBlockedTiles = new();
|
||||
|
||||
// Mark tiles as entered if any were just freed due to airtight/explosion blockers being destroyed.
|
||||
if (FreedTileLists.TryGetValue(iteration, out var freed))
|
||||
{
|
||||
freed.ExceptWith(EnteredBlockedTiles);
|
||||
EnteredBlockedTiles.UnionWith(freed);
|
||||
NewFreedTiles = freed;
|
||||
}
|
||||
else
|
||||
{
|
||||
NewFreedTiles = new();
|
||||
FreedTileLists[iteration] = NewFreedTiles;
|
||||
}
|
||||
|
||||
// Add adjacent tiles
|
||||
if (TileLists.TryGetValue(iteration - 2, out var adjacent))
|
||||
AddNewAdjacentTiles(iteration, adjacent, false);
|
||||
if (FreedTileLists.TryGetValue(iteration - 2, out var delayedAdjacent))
|
||||
AddNewAdjacentTiles(iteration, delayedAdjacent, true);
|
||||
|
||||
// Add diagonal tiles
|
||||
if (TileLists.TryGetValue(iteration - 3, out var diagonal))
|
||||
AddNewDiagonalTiles(iteration, diagonal, false);
|
||||
if (FreedTileLists.TryGetValue(iteration - 3, out var delayedDiagonal))
|
||||
AddNewDiagonalTiles(iteration, delayedDiagonal, true);
|
||||
|
||||
// Add delayed tiles
|
||||
AddDelayedNeighbors(iteration);
|
||||
|
||||
// Tiles from Spaaaace
|
||||
if (gridJump != null)
|
||||
{
|
||||
foreach (var tile in gridJump)
|
||||
{
|
||||
ProcessNewTile(iteration, tile, AtmosDirection.Invalid);
|
||||
}
|
||||
}
|
||||
|
||||
// Store new tiles
|
||||
if (NewTiles.Count != 0)
|
||||
TileLists[iteration] = NewTiles;
|
||||
if (NewBlockedTiles.Count != 0)
|
||||
BlockedTileLists[iteration] = NewBlockedTiles;
|
||||
|
||||
return NewTiles.Count + NewBlockedTiles.Count;
|
||||
}
|
||||
|
||||
protected override void ProcessNewTile(int iteration, Vector2i tile, AtmosDirection entryDirections)
|
||||
{
|
||||
// Is there an airtight blocker on this tile?
|
||||
if (!_airtightMap.TryGetValue(tile, out var tileData))
|
||||
{
|
||||
// No blocker. Ezy. Though maybe this a space tile?
|
||||
|
||||
if (_spaceTiles.Contains(tile))
|
||||
JumpToSpace(tile);
|
||||
else if (ProcessedTiles.Add(tile))
|
||||
NewTiles.Add(tile);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the explosion is entering this new tile from an unblocked direction, we add it directly. Note that because
|
||||
// for space -> grid jumps, we don't have a direction from which the explosion came, we will only assume it is
|
||||
// unblocked if all space-facing directions are unblocked. Though this could eventually be done properly.
|
||||
|
||||
bool blocked;
|
||||
var blockedDirections = tileData.BlockedDirections;
|
||||
if (entryDirections == AtmosDirection.Invalid) // is coming from space?
|
||||
{
|
||||
var spaceDirections = _edgeTiles[tile];
|
||||
blocked = (blockedDirections & spaceDirections) != 0; // at least one space direction is blocked.
|
||||
}
|
||||
else
|
||||
blocked = (blockedDirections & entryDirections) == entryDirections;// **ALL** entry directions are blocked
|
||||
|
||||
if (blocked)
|
||||
{
|
||||
// was this tile already entered from some other direction?
|
||||
if (EnteredBlockedTiles.Contains(tile))
|
||||
return;
|
||||
|
||||
// Did the explosion already attempt to enter this tile from some other direction?
|
||||
if (!UnenteredBlockedTiles.Add(tile))
|
||||
return;
|
||||
|
||||
NewBlockedTiles.Add(tile);
|
||||
|
||||
// At what explosion iteration would this blocker be destroyed?
|
||||
|
||||
if (!tileData.ExplosionTolerance.TryGetValue(_typeID, out var sealIntegrity))
|
||||
sealIntegrity = float.MaxValue; // indestructible airtight entity
|
||||
|
||||
var clearIteration = iteration + (int) MathF.Ceiling(sealIntegrity / _intensityStepSize);
|
||||
if (FreedTileLists.TryGetValue(clearIteration, out var list))
|
||||
list.Add(tile);
|
||||
else
|
||||
FreedTileLists[clearIteration] = new() { tile };
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// was this tile already entered from some other direction?
|
||||
if (!EnteredBlockedTiles.Add(tile))
|
||||
return;
|
||||
|
||||
// Did the explosion already attempt to enter this tile from some other direction?
|
||||
if (UnenteredBlockedTiles.Contains(tile))
|
||||
{
|
||||
NewFreedTiles.Add(tile);
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a completely new tile, and we just so happened to enter it from an unblocked direction.
|
||||
NewTiles.Add(tile);
|
||||
}
|
||||
|
||||
private void JumpToSpace(Vector2i tile)
|
||||
{
|
||||
// Did we already jump/process this tile?
|
||||
if (!_processedSpaceTiles.Add(tile))
|
||||
return;
|
||||
|
||||
if (!_needToTransform)
|
||||
{
|
||||
SpaceJump.Add(tile);
|
||||
return;
|
||||
}
|
||||
|
||||
var center = _matrix.Transform(tile);
|
||||
SpaceJump.Add(new((int) MathF.Floor(center.X + _offset.X), (int) MathF.Floor(center.Y + _offset.Y)));
|
||||
SpaceJump.Add(new((int) MathF.Floor(center.X - _offset.Y), (int) MathF.Floor(center.Y + _offset.X)));
|
||||
SpaceJump.Add(new((int) MathF.Floor(center.X - _offset.X), (int) MathF.Floor(center.Y - _offset.Y)));
|
||||
SpaceJump.Add(new((int) MathF.Floor(center.X + _offset.Y), (int) MathF.Floor(center.Y - _offset.X)));
|
||||
}
|
||||
|
||||
private void AddDelayedNeighbors(int iteration)
|
||||
{
|
||||
if (!_delayedNeighbors.TryGetValue(iteration, out var delayed))
|
||||
return;
|
||||
|
||||
foreach (var (tile, direction) in delayed)
|
||||
{
|
||||
ProcessNewTile(iteration, tile, direction);
|
||||
}
|
||||
|
||||
_delayedNeighbors.Remove(iteration);
|
||||
}
|
||||
|
||||
// Gets the tiles that are directly adjacent to other tiles. If a currently exploding tile has an airtight entity
|
||||
// that blocks the explosion from propagating in some direction, those tiles are added to a list of delayed tiles
|
||||
// that will be added to the explosion in some future iteration.
|
||||
private void AddNewAdjacentTiles(int iteration, IEnumerable<Vector2i> tiles, bool ignoreTileBlockers = false)
|
||||
{
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
var blockedDirections = AtmosDirection.Invalid;
|
||||
float sealIntegrity = 0;
|
||||
|
||||
// Note that if (grid, tile) is not a valid key, then airtight.BlockedDirections will default to 0 (no blocked directions)
|
||||
if (_airtightMap.TryGetValue(tile, out var tileData))
|
||||
{
|
||||
blockedDirections = tileData.BlockedDirections;
|
||||
if (!tileData.ExplosionTolerance.TryGetValue(_typeID, out sealIntegrity))
|
||||
sealIntegrity = float.MaxValue; // indestructible airtight entity
|
||||
}
|
||||
|
||||
// First, yield any neighboring tiles that are not blocked by airtight entities on this tile
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection) (1 << i);
|
||||
if (ignoreTileBlockers || !blockedDirections.IsFlagSet(direction))
|
||||
{
|
||||
ProcessNewTile(iteration, tile.Offset(direction), direction.GetOpposite());
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no blocked directions, we are done with this tile.
|
||||
if (ignoreTileBlockers || blockedDirections == AtmosDirection.Invalid)
|
||||
continue;
|
||||
|
||||
// This tile has one or more airtight entities anchored to it blocking the explosion from traveling in
|
||||
// some directions. First, check whether this blocker can even be destroyed by this explosion?
|
||||
if (sealIntegrity > _maxIntensity || float.IsNaN(sealIntegrity))
|
||||
continue;
|
||||
|
||||
// At what explosion iteration would this blocker be destroyed?
|
||||
var clearIteration = iteration + (int) MathF.Ceiling(sealIntegrity / _intensityStepSize);
|
||||
|
||||
// Get the delayed neighbours list
|
||||
if (!_delayedNeighbors.TryGetValue(clearIteration, out var list))
|
||||
{
|
||||
list = new();
|
||||
_delayedNeighbors[clearIteration] = list;
|
||||
}
|
||||
|
||||
// Check which directions are blocked, and add them to the list.
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection) (1 << i);
|
||||
if (blockedDirections.IsFlagSet(direction))
|
||||
{
|
||||
list.Add((tile.Offset(direction), direction.GetOpposite()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override AtmosDirection GetUnblockedDirectionOrAll(Vector2i tile)
|
||||
{
|
||||
return ~_airtightMap.GetValueOrDefault(tile).BlockedDirections;
|
||||
}
|
||||
}
|
||||
157
Content.Server/Explosion/EntitySystems/SpaceExplosion.cs
Normal file
157
Content.Server/Explosion/EntitySystems/SpaceExplosion.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
internal sealed class SpaceExplosion : TileExplosion
|
||||
{
|
||||
/// <summary>
|
||||
/// The keys of this dictionary correspond to space tiles that intersect a grid. The values have information
|
||||
/// about what grid (which could be more than one), and in what directions the space-based explosion is allowed
|
||||
/// to propagate from this tile.
|
||||
/// </summary>
|
||||
private Dictionary<Vector2i, GridBlockData> _gridBlockMap;
|
||||
|
||||
/// <summary>
|
||||
/// After every iteration, this data set will store all the grid-tiles that were reached as a result of the
|
||||
/// explosion expanding in space.
|
||||
/// </summary>
|
||||
internal Dictionary<GridId, HashSet<Vector2i>> GridJump = new();
|
||||
|
||||
internal SpaceExplosion(ExplosionSystem system, MapId targetMap, GridId? referenceGrid, List<GridId> localGrids)
|
||||
{
|
||||
(_gridBlockMap, var tileSize) = system.TransformGridEdges(targetMap, referenceGrid, localGrids);
|
||||
system.GetUnblockedDirections(_gridBlockMap, tileSize);
|
||||
}
|
||||
|
||||
internal int AddNewTiles(int iteration, HashSet<Vector2i> inputSpaceTiles)
|
||||
{
|
||||
NewTiles = new();
|
||||
NewBlockedTiles = new();
|
||||
NewFreedTiles = new();
|
||||
GridJump = new();
|
||||
|
||||
// Adjacent tiles
|
||||
if (TileLists.TryGetValue(iteration - 2, out var adjacent))
|
||||
AddNewAdjacentTiles(iteration, adjacent);
|
||||
if (FreedTileLists.TryGetValue((iteration - 2) % 3, out var delayedAdjacent))
|
||||
AddNewAdjacentTiles(iteration, delayedAdjacent);
|
||||
|
||||
// Diagonal tiles
|
||||
if (TileLists.TryGetValue(iteration - 3, out var diagonal))
|
||||
AddNewDiagonalTiles(iteration, diagonal);
|
||||
if (FreedTileLists.TryGetValue((iteration - 3) % 3, out var delayedDiagonal))
|
||||
AddNewDiagonalTiles(iteration, delayedDiagonal);
|
||||
|
||||
// Tiles entering space from some grid.
|
||||
foreach (var tile in inputSpaceTiles)
|
||||
{
|
||||
ProcessNewTile(iteration, tile, AtmosDirection.All);
|
||||
}
|
||||
|
||||
// Store new tiles
|
||||
if (NewTiles.Count != 0)
|
||||
TileLists[iteration] = NewTiles;
|
||||
if (NewBlockedTiles.Count != 0)
|
||||
BlockedTileLists[iteration] = NewBlockedTiles;
|
||||
FreedTileLists[iteration % 3] = NewFreedTiles;
|
||||
|
||||
// return new tile count
|
||||
return NewTiles.Count + NewBlockedTiles.Count;
|
||||
}
|
||||
|
||||
private void JumpToGrid(GridBlockData blocker)
|
||||
{
|
||||
foreach (var edge in blocker.BlockingGridEdges)
|
||||
{
|
||||
if (edge.Grid == null) continue;
|
||||
|
||||
if (!GridJump.TryGetValue(edge.Grid.Value, out var set))
|
||||
{
|
||||
set = new();
|
||||
GridJump[edge.Grid.Value] = set;
|
||||
}
|
||||
|
||||
set.Add(edge.Tile);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddNewAdjacentTiles(int iteration, IEnumerable<Vector2i> tiles)
|
||||
{
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
var unblockedDirections = GetUnblockedDirectionOrAll(tile);
|
||||
|
||||
if (unblockedDirections == AtmosDirection.Invalid)
|
||||
continue;
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection) (1 << i);
|
||||
|
||||
if (!unblockedDirections.IsFlagSet(direction))
|
||||
continue; // explosion cannot propagate in this direction. Ever.
|
||||
|
||||
ProcessNewTile(iteration, tile.Offset(direction), direction.GetOpposite());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal override void InitTile(Vector2i initialTile)
|
||||
{
|
||||
base.InitTile(initialTile);
|
||||
|
||||
// It might be the case that the initial space-explosion tile actually overlaps on a grid. In that case we
|
||||
// need to manually add it to the `spaceToGridTiles` dictionary. This would normally be done automatically
|
||||
// during the neighbor finding steps.
|
||||
if (_gridBlockMap.TryGetValue(initialTile, out var blocker))
|
||||
JumpToGrid(blocker);
|
||||
}
|
||||
|
||||
protected override void ProcessNewTile(int iteration, Vector2i tile, AtmosDirection entryDirection)
|
||||
{
|
||||
if (!_gridBlockMap.TryGetValue(tile, out var blocker))
|
||||
{
|
||||
// this tile does not intersect any grids. Add it (if its new) and continue.
|
||||
if (ProcessedTiles.Add(tile))
|
||||
NewTiles.Add(tile);
|
||||
return;
|
||||
}
|
||||
|
||||
// Is the entry to this tile blocked?
|
||||
if ((blocker.UnblockedDirections & entryDirection) == 0)
|
||||
{
|
||||
// was this tile already entered from some other direction?
|
||||
if (EnteredBlockedTiles.Contains(tile))
|
||||
return;
|
||||
|
||||
// Did the explosion already attempt to enter this tile from some other direction?
|
||||
if (!UnenteredBlockedTiles.Add(tile))
|
||||
return;
|
||||
|
||||
// First time the explosion is reaching this tile.
|
||||
NewBlockedTiles.Add(tile);
|
||||
JumpToGrid(blocker);
|
||||
}
|
||||
|
||||
// Was this tile already entered?
|
||||
if (!EnteredBlockedTiles.Add(tile))
|
||||
return;
|
||||
|
||||
// Did the explosion already attempt to enter this tile from some other direction?
|
||||
if (UnenteredBlockedTiles.Contains(tile))
|
||||
{
|
||||
NewFreedTiles.Add(tile);
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a completely new tile, and we just so happened to enter it from an unblocked direction.
|
||||
NewTiles.Add(tile);
|
||||
JumpToGrid(blocker);
|
||||
}
|
||||
|
||||
protected override AtmosDirection GetUnblockedDirectionOrAll(Vector2i tile)
|
||||
{
|
||||
return _gridBlockMap.TryGetValue(tile, out var blocker) ? blocker.UnblockedDirections : AtmosDirection.All;
|
||||
}
|
||||
}
|
||||
116
Content.Server/Explosion/EntitySystems/TileExplosion.cs
Normal file
116
Content.Server/Explosion/EntitySystems/TileExplosion.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using Content.Shared.Atmos;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// This is the base class for <see cref="SpaceExplosion"/> and <see cref="GridExplosion"/>. It just exists to avoid some code duplication, because those classes are generally quite distinct.
|
||||
/// </summary>
|
||||
internal abstract class TileExplosion
|
||||
{
|
||||
// Main tile data sets, mapping iterations onto tile lists
|
||||
internal Dictionary<int, List<Vector2i>> TileLists = new();
|
||||
protected Dictionary<int, List<Vector2i>> BlockedTileLists = new();
|
||||
protected Dictionary<int, HashSet<Vector2i>> FreedTileLists = new();
|
||||
|
||||
// The new tile lists added each iteration. I **could** just pass these along to every function, but IMO it is more
|
||||
// readable if they are just private variables.
|
||||
protected List<Vector2i> NewTiles = default!;
|
||||
protected List<Vector2i> NewBlockedTiles = default!;
|
||||
protected HashSet<Vector2i> NewFreedTiles = default!;
|
||||
|
||||
// HashSets used to ensure uniqueness of tiles. Prevents the explosion from looping back in on itself.
|
||||
protected HashSet<Vector2i> ProcessedTiles = new();
|
||||
protected HashSet<Vector2i> UnenteredBlockedTiles = new();
|
||||
protected HashSet<Vector2i> EnteredBlockedTiles = new();
|
||||
|
||||
internal virtual void InitTile(Vector2i initialTile)
|
||||
{
|
||||
ProcessedTiles.Add(initialTile);
|
||||
TileLists[0] = new() { initialTile };
|
||||
}
|
||||
|
||||
protected abstract void ProcessNewTile(int iteration, Vector2i tile, AtmosDirection entryDirections);
|
||||
|
||||
protected abstract AtmosDirection GetUnblockedDirectionOrAll(Vector2i tile);
|
||||
|
||||
protected void AddNewDiagonalTiles(int iteration, IEnumerable<Vector2i> tiles, bool ignoreLocalBlocker = false)
|
||||
{
|
||||
AtmosDirection entryDirection = AtmosDirection.Invalid;
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
var freeDirections = ignoreLocalBlocker ? AtmosDirection.All : GetUnblockedDirectionOrAll(tile);
|
||||
|
||||
// Get the free directions of the directly adjacent tiles
|
||||
var freeDirectionsN = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.North));
|
||||
var freeDirectionsE = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.East));
|
||||
var freeDirectionsS = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.South));
|
||||
var freeDirectionsW = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.West));
|
||||
|
||||
// North East
|
||||
if (freeDirections.IsFlagSet(AtmosDirection.North) && freeDirectionsN.IsFlagSet(AtmosDirection.SouthEast))
|
||||
entryDirection |= AtmosDirection.West;
|
||||
|
||||
if (freeDirections.IsFlagSet(AtmosDirection.East) && freeDirectionsE.IsFlagSet(AtmosDirection.NorthWest))
|
||||
entryDirection |= AtmosDirection.South;
|
||||
|
||||
if (entryDirection != AtmosDirection.Invalid)
|
||||
{
|
||||
ProcessNewTile(iteration, tile + (1, 1), entryDirection);
|
||||
entryDirection = AtmosDirection.Invalid;
|
||||
}
|
||||
|
||||
// North West
|
||||
if (freeDirections.IsFlagSet(AtmosDirection.North) && freeDirectionsN.IsFlagSet(AtmosDirection.SouthWest))
|
||||
entryDirection |= AtmosDirection.East;
|
||||
|
||||
if (freeDirections.IsFlagSet(AtmosDirection.West) && freeDirectionsW.IsFlagSet(AtmosDirection.NorthEast))
|
||||
entryDirection |= AtmosDirection.West;
|
||||
|
||||
if (entryDirection != AtmosDirection.Invalid)
|
||||
{
|
||||
ProcessNewTile(iteration, tile + (-1, 1), entryDirection);
|
||||
entryDirection = AtmosDirection.Invalid;
|
||||
}
|
||||
|
||||
// South East
|
||||
if (freeDirections.IsFlagSet(AtmosDirection.South) && freeDirectionsS.IsFlagSet(AtmosDirection.NorthEast))
|
||||
entryDirection |= AtmosDirection.West;
|
||||
|
||||
if (freeDirections.IsFlagSet(AtmosDirection.East) && freeDirectionsE.IsFlagSet(AtmosDirection.SouthWest))
|
||||
entryDirection |= AtmosDirection.North;
|
||||
|
||||
if (entryDirection != AtmosDirection.Invalid)
|
||||
{
|
||||
ProcessNewTile(iteration, tile + (1, -1), entryDirection);
|
||||
entryDirection = AtmosDirection.Invalid;
|
||||
}
|
||||
|
||||
// South West
|
||||
if (freeDirections.IsFlagSet(AtmosDirection.South) && freeDirectionsS.IsFlagSet(AtmosDirection.NorthWest))
|
||||
entryDirection |= AtmosDirection.West;
|
||||
|
||||
if (freeDirections.IsFlagSet(AtmosDirection.West) && freeDirectionsW.IsFlagSet(AtmosDirection.SouthEast))
|
||||
entryDirection |= AtmosDirection.North;
|
||||
|
||||
if (entryDirection != AtmosDirection.Invalid)
|
||||
{
|
||||
ProcessNewTile(iteration, tile + (-1, -1), entryDirection);
|
||||
entryDirection = AtmosDirection.Invalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge all tile lists into a single output tile list.
|
||||
/// </summary>
|
||||
internal void CleanUp()
|
||||
{
|
||||
foreach (var (iteration, blocked) in BlockedTileLists)
|
||||
{
|
||||
if (TileLists.TryGetValue(iteration, out var tiles))
|
||||
tiles.AddRange(blocked);
|
||||
else
|
||||
TileLists[iteration] = blocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,33 +65,11 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
SubscribeLocalEvent<ToggleDoorOnTriggerComponent, TriggerEvent>(HandleDoorTrigger);
|
||||
}
|
||||
|
||||
#region Explosions
|
||||
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(uid, out ExplosiveComponent? explosiveComponent)) return;
|
||||
|
||||
Explode(uid, explosiveComponent, args.User);
|
||||
_explosions.TriggerExplosive(uid);
|
||||
}
|
||||
|
||||
// You really shouldn't call this directly (TODO Change that when ExplosionHelper gets changed).
|
||||
public void Explode(EntityUid uid, ExplosiveComponent component, EntityUid? user = null)
|
||||
{
|
||||
if (component.Exploding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
component.Exploding = true;
|
||||
_explosions.SpawnExplosion(uid,
|
||||
component.DevastationRange,
|
||||
component.HeavyImpactRange,
|
||||
component.LightImpactRange,
|
||||
component.FlashRange,
|
||||
user);
|
||||
EntityManager.QueueDeleteEntity(uid);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Flash
|
||||
private void HandleFlashTrigger(EntityUid uid, FlashOnTriggerComponent component, TriggerEvent args)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user