Explosion refactor (#5230)
* Explosions * fix yaml typo and prevent silly UI inputs * oop * Use modified contains() checks And remove IEnumerable * Buff nuke, nerf meteors * optimize the entity lookup stuff a bit * fix tile (0,0) error forgot to do an initial Enumerator.MoveNext(), so the first tile was always the "null" tile. * remove celebration * byte -> int * remove diag edge tile dict * fix one bug but there is another * fix the other bug turns out dividing a ushort leads to rounding errors. Why TF is the grid tile size even a ushort in the first place. * improve edge map * fix minor bug If the initial-explosion tile had an airtight entity on it, the tile was processed twice. * some reviews (transform queries, eye.mapid, and tilesizes in overlays) * Apply suggestions from code review Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * is map paused * GetAllTiles ignores space by default * WriteLine -> WriteError * First -> FirstOrDefault() * default prototype const string * entity query * misc review changes * grid edge max distance * fix fire texture defn bad use of type serializer and ioc-resolves * Remove ExplosionLaunched And allow nukes to throw items towards the outer part of an explosion * no hot-reload disclaimer * replace prototype id string with int index * optimise damage a tiiiiny bit. * entity queries * comments * misc mirror comments * cvars * admin logs * move intensity-per-state to prototype * update tile event to ECS event * git mv * Tweak rpg & minibomb also fix merge bug * you don't exist anymore go away * Fix build Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
|||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.ResourceManagement;
|
||||||
|
using Robust.Shared.Enums;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Content.Client.Administration.UI.SpawnExplosion;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class ExplosionDebugOverlay : Overlay
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||||
|
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
|
||||||
|
public Dictionary<int, List<Vector2i>>? SpaceTiles;
|
||||||
|
public Dictionary<GridId, Dictionary<int, List<Vector2i>>> Tiles = new();
|
||||||
|
public List<float> Intensity = new();
|
||||||
|
public float TotalIntensity;
|
||||||
|
public float Slope;
|
||||||
|
public ushort SpaceTileSize;
|
||||||
|
|
||||||
|
public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace;
|
||||||
|
|
||||||
|
public Matrix3 SpaceMatrix;
|
||||||
|
public MapId Map;
|
||||||
|
|
||||||
|
private readonly Font _font;
|
||||||
|
|
||||||
|
public ExplosionDebugOverlay()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
|
||||||
|
var cache = IoCManager.Resolve<IResourceCache>();
|
||||||
|
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Draw(in OverlayDrawArgs args)
|
||||||
|
{
|
||||||
|
if (Map != args.Viewport.Eye?.Position.MapId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Tiles.Count == 0 && SpaceTiles == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (args.Space)
|
||||||
|
{
|
||||||
|
case OverlaySpace.ScreenSpace:
|
||||||
|
DrawScreen(args);
|
||||||
|
break;
|
||||||
|
case OverlaySpace.WorldSpace:
|
||||||
|
DrawWorld(args);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawScreen(OverlayDrawArgs args)
|
||||||
|
{
|
||||||
|
var handle = args.ScreenHandle;
|
||||||
|
Box2 gridBounds;
|
||||||
|
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
|
||||||
|
|
||||||
|
foreach (var (gridId, tileSets) in Tiles)
|
||||||
|
{
|
||||||
|
if (!_mapManager.TryGetGrid(gridId, out var grid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var gridXform = xformQuery.GetComponent(grid.GridEntityId);
|
||||||
|
var (_, _, matrix, invMatrix) = gridXform.GetWorldPositionRotationMatrixWithInv(xformQuery);
|
||||||
|
gridBounds = invMatrix.TransformBox(args.WorldBounds);
|
||||||
|
DrawText(handle, gridBounds, matrix, tileSets, grid.TileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SpaceTiles == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
gridBounds = Matrix3.Invert(SpaceMatrix).TransformBox(args.WorldBounds);
|
||||||
|
|
||||||
|
DrawText(handle, gridBounds, SpaceMatrix, SpaceTiles, SpaceTileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawText(
|
||||||
|
DrawingHandleScreen handle,
|
||||||
|
Box2 gridBounds,
|
||||||
|
Matrix3 transform,
|
||||||
|
Dictionary<int, List<Vector2i>> tileSets,
|
||||||
|
ushort tileSize)
|
||||||
|
{
|
||||||
|
for (var i = 1; i < Intensity.Count; i++)
|
||||||
|
{
|
||||||
|
if (!tileSets.TryGetValue(i, out var tiles))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var tile in tiles)
|
||||||
|
{
|
||||||
|
var centre = ((Vector2) tile + 0.5f) * tileSize;
|
||||||
|
|
||||||
|
// is the center of this tile visible to the user?
|
||||||
|
if (!gridBounds.Contains(centre))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var worldCenter = transform.Transform(centre);
|
||||||
|
|
||||||
|
var screenCenter = _eyeManager.WorldToScreen(worldCenter);
|
||||||
|
|
||||||
|
if (Intensity![i] > 9)
|
||||||
|
screenCenter += (-12, -8);
|
||||||
|
else
|
||||||
|
screenCenter += (-8, -8);
|
||||||
|
|
||||||
|
handle.DrawString(_font, screenCenter, Intensity![i].ToString("F2"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tileSets.ContainsKey(0))
|
||||||
|
{
|
||||||
|
var epicenter = tileSets[0].First();
|
||||||
|
var worldCenter = transform.Transform(((Vector2) epicenter + 0.5f) * tileSize);
|
||||||
|
var screenCenter = _eyeManager.WorldToScreen(worldCenter) + (-24, -24);
|
||||||
|
var text = $"{Intensity![0]:F2}\nΣ={TotalIntensity:F1}\nΔ={Slope:F1}";
|
||||||
|
handle.DrawString(_font, screenCenter, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawWorld(in OverlayDrawArgs args)
|
||||||
|
{
|
||||||
|
var handle = args.WorldHandle;
|
||||||
|
Box2 gridBounds;
|
||||||
|
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
|
||||||
|
|
||||||
|
foreach (var (gridId, tileSets) in Tiles)
|
||||||
|
{
|
||||||
|
if (!_mapManager.TryGetGrid(gridId, out var grid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var gridXform = xformQuery.GetComponent(grid.GridEntityId);
|
||||||
|
var (_, _, worldMatrix, invWorldMatrix) = gridXform.GetWorldPositionRotationMatrixWithInv(xformQuery);
|
||||||
|
gridBounds = invWorldMatrix.TransformBox(args.WorldBounds);
|
||||||
|
handle.SetTransform(worldMatrix);
|
||||||
|
DrawTiles(handle, gridBounds, tileSets, SpaceTileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SpaceTiles == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
gridBounds = Matrix3.Invert(SpaceMatrix).TransformBox(args.WorldBounds);
|
||||||
|
handle.SetTransform(SpaceMatrix);
|
||||||
|
|
||||||
|
DrawTiles(handle, gridBounds, SpaceTiles, SpaceTileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTiles(
|
||||||
|
DrawingHandleWorld handle,
|
||||||
|
Box2 gridBounds,
|
||||||
|
Dictionary<int, List<Vector2i>> tileSets,
|
||||||
|
ushort tileSize)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Intensity.Count; i++)
|
||||||
|
{
|
||||||
|
var color = ColorMap(Intensity![i]);
|
||||||
|
var colorTransparent = color;
|
||||||
|
colorTransparent.A = 0.2f;
|
||||||
|
|
||||||
|
if (!tileSets.TryGetValue(i, out var tiles))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var tile in tiles)
|
||||||
|
{
|
||||||
|
var centre = ((Vector2) tile + 0.5f) * tileSize;
|
||||||
|
|
||||||
|
// is the center of this tile visible to the user?
|
||||||
|
if (!gridBounds.Contains(centre))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var box = Box2.UnitCentered.Translated(centre);
|
||||||
|
handle.DrawRect(box, color, false);
|
||||||
|
handle.DrawRect(box, colorTransparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color ColorMap(float intensity)
|
||||||
|
{
|
||||||
|
var frac = 1 - intensity / Intensity![0];
|
||||||
|
Color result;
|
||||||
|
if (frac < 0.5f)
|
||||||
|
result = Color.InterpolateBetween(Color.Red, Color.Orange, frac * 2);
|
||||||
|
else
|
||||||
|
result = Color.InterpolateBetween(Color.Orange, Color.Yellow, (frac - 0.5f) * 2);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using Content.Client.Eui;
|
||||||
|
using Content.Shared.Administration;
|
||||||
|
using Content.Shared.Eui;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Client.Administration.UI.SpawnExplosion;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class SpawnExplosionEui : BaseEui
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||||
|
|
||||||
|
private readonly SpawnExplosionWindow _window;
|
||||||
|
private ExplosionDebugOverlay? _debugOverlay;
|
||||||
|
|
||||||
|
public SpawnExplosionEui()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
_window = new SpawnExplosionWindow(this);
|
||||||
|
_window.OnClose += SendClosedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Opened()
|
||||||
|
{
|
||||||
|
base.Opened();
|
||||||
|
_window.OpenCentered();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Closed()
|
||||||
|
{
|
||||||
|
base.Closed();
|
||||||
|
_window.OnClose -= SendClosedMessage;
|
||||||
|
_window.Close();
|
||||||
|
ClearOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendClosedMessage()
|
||||||
|
{
|
||||||
|
SendMessage(new SpawnExplosionEuiMsg.Close());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearOverlay()
|
||||||
|
{
|
||||||
|
if (_overlayManager.HasOverlay<ExplosionDebugOverlay>())
|
||||||
|
_overlayManager.RemoveOverlay<ExplosionDebugOverlay>();
|
||||||
|
_debugOverlay = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestPreviewData(MapCoordinates epicenter, string typeId, float totalIntensity, float intensitySlope, float maxIntensity)
|
||||||
|
{
|
||||||
|
var msg = new SpawnExplosionEuiMsg.PreviewRequest(epicenter, typeId, totalIntensity, intensitySlope, maxIntensity);
|
||||||
|
SendMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Receive explosion preview data and add a client-side explosion preview overlay
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="msg"></param>
|
||||||
|
public override void HandleMessage(EuiMessageBase msg)
|
||||||
|
{
|
||||||
|
if (msg is not SpawnExplosionEuiMsg.PreviewData data)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_debugOverlay == null)
|
||||||
|
{
|
||||||
|
_debugOverlay = new();
|
||||||
|
_overlayManager.AddOverlay(_debugOverlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
_debugOverlay.Tiles = data.Explosion.Tiles;
|
||||||
|
_debugOverlay.SpaceTiles = data.Explosion.SpaceTiles;
|
||||||
|
_debugOverlay.Intensity = data.Explosion.Intensity;
|
||||||
|
_debugOverlay.Slope = data.Slope;
|
||||||
|
_debugOverlay.TotalIntensity = data.TotalIntensity;
|
||||||
|
_debugOverlay.Map = data.Explosion.Epicenter.MapId;
|
||||||
|
_debugOverlay.SpaceMatrix = data.Explosion.SpaceMatrix;
|
||||||
|
_debugOverlay.SpaceTileSize = data.Explosion.SpaceTileSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<DefaultWindow
|
||||||
|
xmlns="https://spacestation14.io"
|
||||||
|
Title="{Loc 'admin-explosion-eui-title'}"
|
||||||
|
SetHeight="380">
|
||||||
|
<BoxContainer Name="MainContainer" Orientation="Vertical">
|
||||||
|
|
||||||
|
<BoxContainer Orientation="Horizontal">
|
||||||
|
<Label Text="{Loc 'admin-explosion-eui-label-type'}" MinSize="120 0" />
|
||||||
|
<OptionButton Name="ExplosionOption" MinSize="70 0" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<BoxContainer Orientation="Horizontal">
|
||||||
|
<Label Text="{Loc 'admin-explosion-eui-label-mapid'}" MinSize="120 0" />
|
||||||
|
<OptionButton Name="MapOptions" MinSize="70 0" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Horizontal">
|
||||||
|
<Label Text="{Loc 'admin-explosion-eui-label-xmap'}" MinSize="120 0" />
|
||||||
|
<FloatSpinBox Name="MapX" MinSize="70 0" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Horizontal">
|
||||||
|
<Label Text="{Loc 'admin-explosion-eui-label-ymap'}" MinSize="120 0" />
|
||||||
|
<FloatSpinBox Name="MapY" MinSize="70 0" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
|
<Button Name="Recentre" Text="{Loc 'admin-explosion-eui-label-current'}" />
|
||||||
|
|
||||||
|
<Control MinSize="0 20"/>
|
||||||
|
|
||||||
|
<CheckBox Name="Preview" Text="{Loc 'admin-explosion-eui-label-preview'}"/>
|
||||||
|
<BoxContainer Orientation="Horizontal">
|
||||||
|
<Label Text="{Loc 'admin-explosion-eui-label-total'}" MinSize="120 0"/>
|
||||||
|
<FloatSpinBox Name="Intensity" MinSize="130 0" HorizontalExpand="True" Value="200"/>
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Horizontal">
|
||||||
|
<Label Text="{Loc 'admin-explosion-eui-label-slope'}" MinSize="120 0" />
|
||||||
|
<FloatSpinBox Name="Slope" MinSize="130 0" HorizontalExpand="True" Value="5"/>
|
||||||
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Horizontal">
|
||||||
|
<Label Text="{Loc 'admin-explosion-eui-label-max'}" MinSize="120 0" />
|
||||||
|
<FloatSpinBox Name="MaxIntensity" MinSize="130 0" HorizontalExpand="True" Value="100"/>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<Control MinSize="0 20"/>
|
||||||
|
|
||||||
|
<Button Name="Spawn" Text="{Loc 'admin-explosion-eui-label-spawn'}" />
|
||||||
|
</BoxContainer>
|
||||||
|
</DefaultWindow>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
using Content.Shared.Explosion;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.Console;
|
||||||
|
using Robust.Client.Player;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||||
|
using static Robust.Client.UserInterface.Controls.OptionButton;
|
||||||
|
|
||||||
|
namespace Content.Client.Administration.UI.SpawnExplosion;
|
||||||
|
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed partial class SpawnExplosionWindow : DefaultWindow
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IClientConsoleHost _conHost = default!;
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||||
|
|
||||||
|
|
||||||
|
private readonly SpawnExplosionEui _eui;
|
||||||
|
private List<MapId> _mapData = new();
|
||||||
|
private List<string> _explosionTypes = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to prevent unnecessary preview updates when setting fields (e.g., updating position)..
|
||||||
|
/// </summary>
|
||||||
|
private bool _pausePreview;
|
||||||
|
|
||||||
|
public SpawnExplosionWindow(SpawnExplosionEui eui)
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
_eui = eui;
|
||||||
|
|
||||||
|
ExplosionOption.OnItemSelected += ExplosionSelected;
|
||||||
|
MapOptions.OnItemSelected += MapSelected;
|
||||||
|
Recentre.OnPressed += (_) => SetLocation();
|
||||||
|
Spawn.OnPressed += SubmitButtonOnOnPressed;
|
||||||
|
|
||||||
|
Preview.OnToggled += (_) => UpdatePreview();
|
||||||
|
MapX.OnValueChanged += (_) => UpdatePreview();
|
||||||
|
MapY.OnValueChanged += (_) => UpdatePreview();
|
||||||
|
Intensity.OnValueChanged += (_) => UpdatePreview();
|
||||||
|
Slope.OnValueChanged += (_) => UpdatePreview();
|
||||||
|
MaxIntensity.OnValueChanged += (_) => UpdatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExplosionSelected(ItemSelectedEventArgs args)
|
||||||
|
{
|
||||||
|
ExplosionOption.SelectId(args.Id);
|
||||||
|
UpdatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MapSelected(ItemSelectedEventArgs args)
|
||||||
|
{
|
||||||
|
MapOptions.SelectId(args.Id);
|
||||||
|
UpdatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void EnteredTree()
|
||||||
|
{
|
||||||
|
SetLocation();
|
||||||
|
UpdateExplosionTypeOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateExplosionTypeOptions()
|
||||||
|
{
|
||||||
|
_explosionTypes.Clear();
|
||||||
|
ExplosionOption.Clear();
|
||||||
|
foreach (var type in _prototypeManager.EnumeratePrototypes<ExplosionPrototype>())
|
||||||
|
{
|
||||||
|
_explosionTypes.Add(type.ID);
|
||||||
|
ExplosionOption.AddItem(type.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMapOptions()
|
||||||
|
{
|
||||||
|
_mapData.Clear();
|
||||||
|
MapOptions.Clear();
|
||||||
|
foreach (var map in _mapManager.GetAllMapIds())
|
||||||
|
{
|
||||||
|
_mapData.Add(map);
|
||||||
|
MapOptions.AddItem(map.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the current grid & indices based on the attached entities current location.
|
||||||
|
/// </summary>
|
||||||
|
private void SetLocation()
|
||||||
|
{
|
||||||
|
UpdateMapOptions();
|
||||||
|
|
||||||
|
if (!_entMan.TryGetComponent(_playerManager.LocalPlayer?.ControlledEntity, out TransformComponent? transform))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_pausePreview = true;
|
||||||
|
MapOptions.Select(_mapData.IndexOf(transform.MapID));
|
||||||
|
(MapX.Value, MapY.Value) = transform.MapPosition.Position;
|
||||||
|
_pausePreview = false;
|
||||||
|
|
||||||
|
UpdatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePreview()
|
||||||
|
{
|
||||||
|
if (_pausePreview)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!Preview.Pressed)
|
||||||
|
{
|
||||||
|
_eui.ClearOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MapCoordinates coords = new((MapX.Value, MapY.Value), _mapData[MapOptions.SelectedId]);
|
||||||
|
var explosionType = _explosionTypes[ExplosionOption.SelectedId];
|
||||||
|
_eui.RequestPreviewData(coords, explosionType, Intensity.Value, Slope.Value, MaxIntensity.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubmitButtonOnOnPressed(ButtonEventArgs args)
|
||||||
|
{
|
||||||
|
// need to make room to view the fireworks
|
||||||
|
Preview.Pressed = false;
|
||||||
|
_eui.ClearOverlay();
|
||||||
|
|
||||||
|
// for the actual explosion, we will just re-use the explosion command.
|
||||||
|
// so assemble command arguments:
|
||||||
|
var mapId = _mapData[MapOptions.SelectedId];
|
||||||
|
var explosionType = _explosionTypes[ExplosionOption.SelectedId];
|
||||||
|
var cmd = $"explosion {Intensity.Value} {Slope.Value} {MaxIntensity.Value} {MapX.Value} {MapY.Value} {mapId} {explosionType}";
|
||||||
|
|
||||||
|
_conHost.ExecuteCommand(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ namespace Content.Client.Entry
|
|||||||
"Temperature",
|
"Temperature",
|
||||||
"AtmosExposed",
|
"AtmosExposed",
|
||||||
"Explosive",
|
"Explosive",
|
||||||
|
"ExplosionResistance",
|
||||||
"Vocal",
|
"Vocal",
|
||||||
"OnUseTimerTrigger",
|
"OnUseTimerTrigger",
|
||||||
"WarpPoint",
|
"WarpPoint",
|
||||||
@@ -267,7 +268,6 @@ namespace Content.Client.Entry
|
|||||||
"RandomSpawner",
|
"RandomSpawner",
|
||||||
"SpawnAfterInteract",
|
"SpawnAfterInteract",
|
||||||
"DisassembleOnAltVerb",
|
"DisassembleOnAltVerb",
|
||||||
"ExplosionLaunched",
|
|
||||||
"BeingCloned",
|
"BeingCloned",
|
||||||
"Advertise",
|
"Advertise",
|
||||||
"Bible",
|
"Bible",
|
||||||
|
|||||||
119
Content.Client/Explosion/ExplosionOverlay.cs
Normal file
119
Content.Client/Explosion/ExplosionOverlay.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Shared.Enums;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Client.Explosion;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class ExplosionOverlay : Overlay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The explosion that needs to be drawn. This explosion is currently being processed by the server and
|
||||||
|
/// expanding outwards.
|
||||||
|
/// </summary>
|
||||||
|
internal Explosion? ActiveExplosion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This index specifies what parts of the currently expanding explosion should be drawn.
|
||||||
|
/// </summary>
|
||||||
|
public int Index;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// These explosions have finished expanding, but we will draw for a few more frames. This is important for
|
||||||
|
/// small explosions, as otherwise they disappear far too quickly.
|
||||||
|
/// </summary>
|
||||||
|
internal List<Explosion> CompletedExplosions = new ();
|
||||||
|
|
||||||
|
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||||
|
|
||||||
|
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||||
|
|
||||||
|
private ShaderInstance _shader;
|
||||||
|
|
||||||
|
public ExplosionOverlay()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
_shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Draw(in OverlayDrawArgs args)
|
||||||
|
{
|
||||||
|
var drawHandle = args.WorldHandle;
|
||||||
|
drawHandle.UseShader(_shader);
|
||||||
|
|
||||||
|
var xforms = _entMan.GetEntityQuery<TransformComponent>();
|
||||||
|
|
||||||
|
if (ActiveExplosion != null && ActiveExplosion.Map == args.Viewport.Eye?.Position.MapId)
|
||||||
|
{
|
||||||
|
DrawExplosion(drawHandle, args.WorldBounds, ActiveExplosion, Index, xforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var exp in CompletedExplosions)
|
||||||
|
{
|
||||||
|
if (exp.Map == args.Viewport.Eye?.Position.MapId)
|
||||||
|
DrawExplosion(drawHandle, args.WorldBounds, exp, exp.Intensity.Count, xforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHandle.SetTransform(Matrix3.Identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawExplosion(DrawingHandleWorld drawHandle, Box2Rotated worldBounds, Explosion exp, int index, EntityQuery<TransformComponent> xforms)
|
||||||
|
{
|
||||||
|
Box2 gridBounds;
|
||||||
|
foreach (var (gridId, tiles) in exp.Tiles)
|
||||||
|
{
|
||||||
|
if (!_mapManager.TryGetGrid(gridId, out var grid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var xform = xforms.GetComponent(grid.GridEntityId);
|
||||||
|
var (_, _, worldMatrix, invWorldMatrix) = xform.GetWorldPositionRotationMatrixWithInv(xforms);
|
||||||
|
|
||||||
|
gridBounds = invWorldMatrix.TransformBox(worldBounds);
|
||||||
|
drawHandle.SetTransform(worldMatrix);
|
||||||
|
|
||||||
|
DrawTiles(drawHandle, gridBounds, index, tiles, exp, grid.TileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exp.SpaceTiles == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
gridBounds = Matrix3.Invert(exp.SpaceMatrix).TransformBox(worldBounds);
|
||||||
|
drawHandle.SetTransform(exp.SpaceMatrix);
|
||||||
|
|
||||||
|
DrawTiles(drawHandle, gridBounds, index, exp.SpaceTiles, exp, exp.SpaceTileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTiles(
|
||||||
|
DrawingHandleWorld drawHandle,
|
||||||
|
Box2 gridBounds,
|
||||||
|
int index,
|
||||||
|
Dictionary<int, List<Vector2i>> tileSets,
|
||||||
|
Explosion exp,
|
||||||
|
ushort tileSize)
|
||||||
|
{
|
||||||
|
for (var j = 0; j < index; j++)
|
||||||
|
{
|
||||||
|
if (!tileSets.TryGetValue(j, out var tiles))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var frameIndex = (int) Math.Min(exp.Intensity[j] / exp.IntensityPerState, exp.FireFrames.Count - 1);
|
||||||
|
var frames = exp.FireFrames[frameIndex];
|
||||||
|
|
||||||
|
foreach (var tile in tiles)
|
||||||
|
{
|
||||||
|
Vector2 centre = ((Vector2) tile + 0.5f) * tileSize;
|
||||||
|
|
||||||
|
if (!gridBounds.Contains(centre))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var texture = _robustRandom.Pick(frames);
|
||||||
|
drawHandle.DrawTextureRect(texture, Box2.CenteredAround(centre, (tileSize, tileSize)), exp.FireColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
211
Content.Client/Explosion/ExplosionOverlaySystem.cs
Normal file
211
Content.Client/Explosion/ExplosionOverlaySystem.cs
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
using Content.Shared.CCVar;
|
||||||
|
using Content.Shared.Explosion;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.ResourceManagement;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Client.Explosion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This system is responsible for showing the client-side explosion effects (light source & fire-overlay). The
|
||||||
|
/// fire overlay code is just a bastardized version of the atmos plasma fire overlay and uses the same texture.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExplosionOverlaySystem : EntitySystem
|
||||||
|
{
|
||||||
|
private ExplosionOverlay _overlay = default!;
|
||||||
|
|
||||||
|
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||||
|
[Dependency] private readonly IMapManager _mapMan = default!;
|
||||||
|
[Dependency] private readonly IResourceCache _resCache = default!;
|
||||||
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For how many seconds should an explosion stay on-screen once it has finished expanding?
|
||||||
|
/// </summary>
|
||||||
|
public float ExplosionPersistence = 0.3f;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeNetworkEvent<ExplosionEvent>(OnExplosion);
|
||||||
|
SubscribeNetworkEvent<ExplosionOverlayUpdateEvent>(HandleExplosionUpdate);
|
||||||
|
|
||||||
|
_cfg.OnValueChanged(CCVars.ExplosionPersistence, SetExplosionPersistence, true);
|
||||||
|
|
||||||
|
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||||
|
_overlay = new ExplosionOverlay();
|
||||||
|
if (!overlayManager.HasOverlay<ExplosionOverlay>())
|
||||||
|
overlayManager.AddOverlay(_overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetExplosionPersistence(float value) => ExplosionPersistence = value;
|
||||||
|
|
||||||
|
public override void FrameUpdate(float frameTime)
|
||||||
|
{
|
||||||
|
base.FrameUpdate(frameTime);
|
||||||
|
|
||||||
|
// increment the lifetime of completed explosions, and remove them if they have been ons screen for more
|
||||||
|
// than ExplosionPersistence seconds
|
||||||
|
for (int i = _overlay.CompletedExplosions.Count - 1; i>= 0; i--)
|
||||||
|
{
|
||||||
|
var explosion = _overlay.CompletedExplosions[i];
|
||||||
|
|
||||||
|
if (_mapMan.IsMapPaused(explosion.Map))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
explosion.Lifetime += frameTime;
|
||||||
|
|
||||||
|
if (explosion.Lifetime >= ExplosionPersistence)
|
||||||
|
{
|
||||||
|
EntityManager.QueueDeleteEntity(explosion.LightEntity);
|
||||||
|
|
||||||
|
// Remove-swap
|
||||||
|
_overlay.CompletedExplosions[i] = _overlay.CompletedExplosions[^1];
|
||||||
|
_overlay.CompletedExplosions.RemoveAt(_overlay.CompletedExplosions.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The server has processed some explosion. This updates the client-side overlay so that the area covered
|
||||||
|
/// by the fire-visual matches up with the area that the explosion has affected.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleExplosionUpdate(ExplosionOverlayUpdateEvent args)
|
||||||
|
{
|
||||||
|
if (args.ExplosionId != _overlay.ActiveExplosion?.Explosionid && !IsNewer(args.ExplosionId))
|
||||||
|
{
|
||||||
|
// out of order events. Ignore.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_overlay.Index = args.Index;
|
||||||
|
|
||||||
|
if (_overlay.ActiveExplosion == null)
|
||||||
|
{
|
||||||
|
// no active explosion... events out of order?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Index != int.MaxValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// the explosion has finished expanding
|
||||||
|
_overlay.Index = 0;
|
||||||
|
_overlay.CompletedExplosions.Add(_overlay.ActiveExplosion);
|
||||||
|
_overlay.ActiveExplosion = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A new explosion occurred. This prepares the client-side light entity and stores the
|
||||||
|
/// explosion/fire-effect overlay data.
|
||||||
|
/// </summary>
|
||||||
|
private void OnExplosion(ExplosionEvent args)
|
||||||
|
{
|
||||||
|
if (!_protoMan.TryIndex(args.TypeID, out ExplosionPrototype? type))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// spawn in a light source at the epicenter
|
||||||
|
var lightEntity = Spawn("ExplosionLight", args.Epicenter);
|
||||||
|
var light = EnsureComp<PointLightComponent>(lightEntity);
|
||||||
|
light.Energy = light.Radius = args.Intensity.Count;
|
||||||
|
light.Color = type.LightColor;
|
||||||
|
|
||||||
|
if (_overlay.ActiveExplosion == null)
|
||||||
|
{
|
||||||
|
_overlay.ActiveExplosion = new(args, type, lightEntity, _resCache);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have a currently active explosion. Can happen when events are received out of order. either multiple
|
||||||
|
// explosions are happening in one tick, or a new explosion was received before the event telling us the old one
|
||||||
|
// finished got through.
|
||||||
|
|
||||||
|
if (IsNewer(args.ExplosionId))
|
||||||
|
{
|
||||||
|
// This is a newer explosion. Add the old-currently-active explosions to the completed list
|
||||||
|
_overlay.CompletedExplosions.Add(_overlay.ActiveExplosion);
|
||||||
|
_overlay.ActiveExplosion = new(args, type, lightEntity, _resCache);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// explosions were out of order. keep the active one, and directly add the received one to the completed
|
||||||
|
// list.
|
||||||
|
_overlay.CompletedExplosions.Add(new(args, type, lightEntity, _resCache));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsNewer(int explosionId)
|
||||||
|
{
|
||||||
|
if (_overlay.ActiveExplosion == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// If we ever get servers stable enough to live this long, the explosion Id int might overflow.
|
||||||
|
return _overlay.ActiveExplosion.Explosionid < explosionId
|
||||||
|
|| _overlay.ActiveExplosion.Explosionid > int.MaxValue/2 && explosionId < int.MinValue/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
base.Shutdown();
|
||||||
|
|
||||||
|
_cfg.UnsubValueChanged(CCVars.ExplosionPersistence, SetExplosionPersistence);
|
||||||
|
|
||||||
|
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||||
|
if (overlayManager.HasOverlay<ExplosionOverlay>())
|
||||||
|
overlayManager.RemoveOverlay<ExplosionOverlay>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class Explosion
|
||||||
|
{
|
||||||
|
public readonly Dictionary<int, List<Vector2i>>? SpaceTiles;
|
||||||
|
public readonly Dictionary<GridId, Dictionary<int, List<Vector2i>>> Tiles;
|
||||||
|
public readonly List<float> Intensity;
|
||||||
|
public readonly EntityUid LightEntity;
|
||||||
|
public readonly MapId Map;
|
||||||
|
public readonly int Explosionid;
|
||||||
|
public readonly ushort SpaceTileSize;
|
||||||
|
public readonly float IntensityPerState;
|
||||||
|
|
||||||
|
public readonly Matrix3 SpaceMatrix;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long have we been drawing this explosion, starting from the time the explosion was fully drawn.
|
||||||
|
/// </summary>
|
||||||
|
public float Lifetime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The textures used for the explosion fire effect. Each fire-state is associated with an explosion
|
||||||
|
/// intensity range, and each stat itself has several textures.
|
||||||
|
/// </summary>
|
||||||
|
public readonly List<Texture[]> FireFrames = new();
|
||||||
|
|
||||||
|
public readonly Color? FireColor;
|
||||||
|
|
||||||
|
internal Explosion(ExplosionEvent args, ExplosionPrototype type, EntityUid lightEntity, IResourceCache resCache)
|
||||||
|
{
|
||||||
|
Map = args.Epicenter.MapId;
|
||||||
|
SpaceTiles = args.SpaceTiles;
|
||||||
|
Tiles = args.Tiles;
|
||||||
|
Intensity = args.Intensity;
|
||||||
|
SpaceMatrix = args.SpaceMatrix;
|
||||||
|
Explosionid = args.ExplosionId;
|
||||||
|
FireColor = type.FireColor;
|
||||||
|
LightEntity = lightEntity;
|
||||||
|
SpaceTileSize = args.SpaceTileSize;
|
||||||
|
IntensityPerState = type.IntensityPerState;
|
||||||
|
|
||||||
|
var fireRsi = resCache.GetResource<RSIResource>(type.TexturePath).RSI;
|
||||||
|
foreach (var state in fireRsi)
|
||||||
|
{
|
||||||
|
FireFrames.Add(state.GetFrames(RSI.State.Direction.South));
|
||||||
|
if (FireFrames.Count == type.FireStates)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,22 +162,21 @@ namespace Content.Server.AME
|
|||||||
{
|
{
|
||||||
if(_cores.Count < 1 || MasterController == null) { return; }
|
if(_cores.Count < 1 || MasterController == null) { return; }
|
||||||
|
|
||||||
var intensity = 0;
|
float radius = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* todo: add an exact to the shielding and make this find the core closest to the controller
|
* todo: add an exact to the shielding and make this find the core closest to the controller
|
||||||
* so they chain explode, after helpers have been added to make it not cancer
|
* so they chain explode, after helpers have been added to make it not cancer
|
||||||
*/
|
*/
|
||||||
var epicenter = _cores.First();
|
|
||||||
|
|
||||||
foreach (AMEShieldComponent core in _cores)
|
foreach (AMEShieldComponent core in _cores)
|
||||||
{
|
{
|
||||||
intensity += MasterController.InjectionAmount;
|
radius += MasterController.InjectionAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
intensity = Math.Min(intensity, 8);
|
radius *= 2;
|
||||||
|
radius = Math.Min(radius, 8);
|
||||||
EntitySystem.Get<ExplosionSystem>().SpawnExplosion(epicenter.Owner, intensity / 2, intensity, intensity * 2, intensity * 3);
|
EntitySystem.Get<ExplosionSystem>().TriggerExplosive(MasterController.Owner, radius: radius, delete: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace Content.Server.Administration
|
|||||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
[Dependency] private readonly EuiManager _euiManager = default!;
|
[Dependency] private readonly EuiManager _euiManager = default!;
|
||||||
[Dependency] private readonly ExplosionSystem _explosions = default!;
|
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
|
||||||
[Dependency] private readonly GhostRoleSystem _ghostRoleSystem = default!;
|
[Dependency] private readonly GhostRoleSystem _ghostRoleSystem = default!;
|
||||||
[Dependency] private readonly ArtifactSystem _artifactSystem = default!;
|
[Dependency] private readonly ArtifactSystem _artifactSystem = default!;
|
||||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||||
@@ -151,8 +151,11 @@ namespace Content.Server.Administration
|
|||||||
verb.Category = VerbCategory.Admin;
|
verb.Category = VerbCategory.Admin;
|
||||||
verb.Act = () =>
|
verb.Act = () =>
|
||||||
{
|
{
|
||||||
var coords = Transform(args.Target).Coordinates;
|
var coords = Transform(args.Target).MapPosition;
|
||||||
Timer.Spawn(_gameTiming.TickPeriod, () => _explosions.SpawnExplosion(coords, 0, 1, 2, 1), CancellationToken.None);
|
Timer.Spawn(_gameTiming.TickPeriod,
|
||||||
|
() => _explosionSystem.QueueExplosion(coords, ExplosionSystem.DefaultExplosionPrototypeId, 30, 4, 8),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
if (TryComp(args.Target, out SharedBodyComponent? body))
|
if (TryComp(args.Target, out SharedBodyComponent? body))
|
||||||
{
|
{
|
||||||
body.Gib();
|
body.Gib();
|
||||||
|
|||||||
@@ -1,42 +1,136 @@
|
|||||||
|
using Content.Server.Administration.UI;
|
||||||
|
using Content.Server.EUI;
|
||||||
using Content.Server.Explosion.EntitySystems;
|
using Content.Server.Explosion.EntitySystems;
|
||||||
using Content.Shared.Administration;
|
using Content.Shared.Administration;
|
||||||
|
using Content.Shared.Explosion;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Content.Server.Administration.Commands
|
namespace Content.Server.Administration.Commands;
|
||||||
|
|
||||||
|
[AdminCommand(AdminFlags.Fun)]
|
||||||
|
public sealed class OpenExplosionEui : IConsoleCommand
|
||||||
{
|
{
|
||||||
[AdminCommand(AdminFlags.Fun)]
|
public string Command => "explosionui";
|
||||||
public sealed class ExplosionCommand : IConsoleCommand
|
public string Description => "Opens a window for easy access to station destruction";
|
||||||
{
|
public string Help => $"Usage: {Command}";
|
||||||
public string Command => "explode";
|
|
||||||
public string Description => "Train go boom";
|
|
||||||
public string Help => "Usage: explode <x> <y> <dev> <heavy> <light> <flash>\n" +
|
|
||||||
"The explosion happens on the same map as the user.";
|
|
||||||
|
|
||||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
{
|
{
|
||||||
var player = shell.Player as IPlayerSession;
|
var player = shell.Player as IPlayerSession;
|
||||||
if (player?.AttachedEntity is not {Valid: true} playerEntity)
|
if (player == null)
|
||||||
{
|
{
|
||||||
shell.WriteLine("You must have an attached entity.");
|
shell.WriteError("This does not work from the server console.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var x = float.Parse(args[0]);
|
var eui = IoCManager.Resolve<EuiManager>();
|
||||||
var y = float.Parse(args[1]);
|
var ui = new SpawnExplosionEui();
|
||||||
|
eui.OpenEui(ui, player);
|
||||||
var dev = int.Parse(args[2]);
|
}
|
||||||
var hvy = int.Parse(args[3]);
|
}
|
||||||
var lgh = int.Parse(args[4]);
|
|
||||||
var fla = int.Parse(args[5]);
|
[AdminCommand(AdminFlags.Fun)] // for the admin. Not so much for anyone else.
|
||||||
|
public sealed class ExplosionCommand : IConsoleCommand
|
||||||
var mapTransform = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(playerEntity).GetMapTransform();
|
{
|
||||||
var coords = new EntityCoordinates(mapTransform.Owner, x, y);
|
public string Command => "explosion";
|
||||||
|
public string Description => "Train go boom";
|
||||||
EntitySystem.Get<ExplosionSystem>().SpawnExplosion(coords, dev, hvy, lgh, fla);
|
|
||||||
}
|
// Note that if you change the arguments, you should also update the client-side SpawnExplosionWindow, as that just
|
||||||
|
// uses this command.
|
||||||
|
public string Help => "Usage: explosion [intensity] [slope] [maxIntensity] [x y] [mapId] [prototypeId]";
|
||||||
|
|
||||||
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length == 0 || args.Length == 4 || args.Length > 7)
|
||||||
|
{
|
||||||
|
shell.WriteError("Wrong number of arguments.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!float.TryParse(args[0], out var intensity))
|
||||||
|
{
|
||||||
|
shell.WriteError($"Failed to parse intensity: {args[0]}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float slope = 5;
|
||||||
|
if (args.Length > 1 && !float.TryParse(args[1], out slope))
|
||||||
|
{
|
||||||
|
shell.WriteError($"Failed to parse float: {args[1]}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float maxIntensity = 100;
|
||||||
|
if (args.Length > 2 && !float.TryParse(args[2], out maxIntensity))
|
||||||
|
{
|
||||||
|
shell.WriteError($"Failed to parse float: {args[2]}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float x = 0, y = 0;
|
||||||
|
if (args.Length > 4)
|
||||||
|
{
|
||||||
|
if (!float.TryParse(args[3], out x) ||
|
||||||
|
!float.TryParse(args[4], out y))
|
||||||
|
{
|
||||||
|
shell.WriteError($"Failed to parse coordinates: {(args[3], args[4])}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MapCoordinates coords;
|
||||||
|
if (args.Length > 5)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(args[5], out var parsed))
|
||||||
|
{
|
||||||
|
shell.WriteError($"Failed to parse map ID: {args[5]}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
coords = new MapCoordinates((x, y), new(parsed));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// attempt to find the player's current position
|
||||||
|
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||||
|
if (!entMan.TryGetComponent(shell.Player?.AttachedEntity, out TransformComponent? xform))
|
||||||
|
{
|
||||||
|
shell.WriteError($"Failed get default coordinates/map via player's transform. Need to specify explicitly.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Length > 4)
|
||||||
|
coords = new MapCoordinates((x, y), xform.MapID);
|
||||||
|
else
|
||||||
|
coords = xform.MapPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExplosionPrototype? type;
|
||||||
|
var protoMan = IoCManager.Resolve<IPrototypeManager>();
|
||||||
|
if (args.Length > 6)
|
||||||
|
{
|
||||||
|
if (!protoMan.TryIndex(args[6], out type))
|
||||||
|
{
|
||||||
|
shell.WriteError($"Unknown explosion prototype: {args[6]}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// no prototype was specified, so lets default to whichever one was defined first
|
||||||
|
type = protoMan.EnumeratePrototypes<ExplosionPrototype>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
shell.WriteError($"Prototype manager has no explosion prototypes?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||||
|
sysMan.GetEntitySystem<ExplosionSystem>().QueueExplosion(coords, type.ID, intensity, slope, maxIntensity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
Content.Server/Administration/UI/SpawnExplosionEui.cs
Normal file
39
Content.Server/Administration/UI/SpawnExplosionEui.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Content.Server.EUI;
|
||||||
|
using Content.Server.Explosion.EntitySystems;
|
||||||
|
using Content.Shared.Administration;
|
||||||
|
using Content.Shared.Eui;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace Content.Server.Administration.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Admin Eui for spawning and preview-ing explosions
|
||||||
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class SpawnExplosionEui : BaseEui
|
||||||
|
{
|
||||||
|
public override void HandleMessage(EuiMessageBase msg)
|
||||||
|
{
|
||||||
|
if (msg is SpawnExplosionEuiMsg.Close)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg is not SpawnExplosionEuiMsg.PreviewRequest request)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (request.TotalIntensity <= 0 || request.IntensitySlope <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var explosion = EntitySystem.Get<ExplosionSystem>().GenerateExplosionPreview(request);
|
||||||
|
|
||||||
|
if (explosion == null)
|
||||||
|
{
|
||||||
|
Logger.Error("Failed to generate explosion preview.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendMessage(new SpawnExplosionEuiMsg.PreviewData(explosion, request.IntensitySlope, request.TotalIntensity));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -230,14 +230,14 @@ namespace Content.Server.Atmos.Components
|
|||||||
var range = (pressure - TankFragmentPressure) / TankFragmentScale;
|
var range = (pressure - TankFragmentPressure) / TankFragmentScale;
|
||||||
|
|
||||||
// Let's cap the explosion, yeah?
|
// Let's cap the explosion, yeah?
|
||||||
|
// !1984
|
||||||
if (range > MaxExplosionRange)
|
if (range > MaxExplosionRange)
|
||||||
{
|
{
|
||||||
range = MaxExplosionRange;
|
range = MaxExplosionRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
EntitySystem.Get<ExplosionSystem>().SpawnExplosion(Owner, (int) (range * 0.25f), (int) (range * 0.5f), (int) (range * 1.5f), 1);
|
EntitySystem.Get<ExplosionSystem>().TriggerExplosive(Owner, radius: range);
|
||||||
|
|
||||||
_entMan.QueueDeleteEntity(Owner);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Server.Atmos.Components;
|
using Content.Server.Atmos.Components;
|
||||||
|
using Content.Server.Explosion.EntitySystems;
|
||||||
using Content.Server.Kudzu;
|
using Content.Server.Kudzu;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
@@ -14,6 +15,7 @@ namespace Content.Server.Atmos.EntitySystems
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||||
|
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -104,6 +106,9 @@ namespace Content.Server.Atmos.EntitySystems
|
|||||||
if (!gridId.IsValid())
|
if (!gridId.IsValid())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var query = EntityManager.GetEntityQuery<AirtightComponent>();
|
||||||
|
_explosionSystem.UpdateAirtightMap(gridId, pos, query);
|
||||||
|
// TODO make atmos system use query
|
||||||
_atmosphereSystem.UpdateAdjacent(gridId, pos);
|
_atmosphereSystem.UpdateAdjacent(gridId, pos);
|
||||||
_atmosphereSystem.InvalidateTile(gridId, pos);
|
_atmosphereSystem.InvalidateTile(gridId, pos);
|
||||||
|
|
||||||
|
|||||||
@@ -1,78 +1,68 @@
|
|||||||
using System;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Content.Server.Chemistry.Components.SolutionManager;
|
|
||||||
using Content.Server.Explosion.EntitySystems;
|
using Content.Server.Explosion.EntitySystems;
|
||||||
using Content.Shared.Administration.Logs;
|
|
||||||
using Content.Shared.Chemistry.Reagent;
|
using Content.Shared.Chemistry.Reagent;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Robust.Shared.GameObjects;
|
using Content.Shared.Explosion;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
|
||||||
namespace Content.Server.Chemistry.ReactionEffects
|
namespace Content.Server.Chemistry.ReactionEffects
|
||||||
{
|
{
|
||||||
[DataDefinition]
|
[DataDefinition]
|
||||||
public sealed class ExplosionReactionEffect : ReagentEffect
|
public class ExplosionReactionEffect : ReagentEffect
|
||||||
{
|
{
|
||||||
[DataField("devastationRange")]
|
/// <summary>
|
||||||
|
/// The type of explosion. Determines damage types and tile break chance scaling.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("explosionType", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))]
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
private float _devastationRange = 1;
|
public string ExplosionType = default!;
|
||||||
|
|
||||||
[DataField("heavyImpactRange")]
|
|
||||||
[JsonIgnore]
|
|
||||||
private float _heavyImpactRange = 2;
|
|
||||||
|
|
||||||
[DataField("lightImpactRange")]
|
|
||||||
[JsonIgnore]
|
|
||||||
private float _lightImpactRange = 3;
|
|
||||||
|
|
||||||
[DataField("flashRange")]
|
|
||||||
[JsonIgnore]
|
|
||||||
private float _flashRange;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount.
|
/// The max intensity the explosion can have at a given tile. Places an upper limit of damage and tile break
|
||||||
|
/// chance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("scaled")]
|
[DataField("maxIntensity")]
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
private bool _scaled;
|
public float MaxIntensity = 5;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maximum scaling on ranges. For example, if it equals 5, then it won't scaled anywhere past
|
/// How quickly intensity drops off as you move away from the epicenter
|
||||||
/// 5 times the minimum reactant amount.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("maxScale")]
|
[DataField("intensitySlope")]
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
private float _maxScale = 1;
|
public float IntensitySlope = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum total intensity that this chemical reaction can achieve. Basically here to prevent people
|
||||||
|
/// from creating a nuke by collecting enough potassium and water.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A slope of 1 and MaxTotalIntensity of 100 corresponds to a radius of around 4.5 tiles.
|
||||||
|
/// </remarks>
|
||||||
|
[DataField("maxTotalIntensity")]
|
||||||
|
[JsonIgnore]
|
||||||
|
public float MaxTotalIntensity = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The intensity of the explosion per unit reaction.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("intensityPerUnit")]
|
||||||
|
[JsonIgnore]
|
||||||
|
public float IntensityPerUnit = 1;
|
||||||
|
|
||||||
public override bool ShouldLog => true;
|
public override bool ShouldLog => true;
|
||||||
public override LogImpact LogImpact => LogImpact.High;
|
public override LogImpact LogImpact => LogImpact.High;
|
||||||
|
|
||||||
public override void Effect(ReagentEffectArgs args)
|
public override void Effect(ReagentEffectArgs args)
|
||||||
{
|
{
|
||||||
var floatIntensity = (float) args.Quantity;
|
var intensity = MathF.Min((float) args.Quantity * IntensityPerUnit, MaxTotalIntensity);
|
||||||
|
|
||||||
if (!args.EntityManager.HasComponent<SolutionContainerManagerComponent>(args.SolutionEntity))
|
EntitySystem.Get<ExplosionSystem>().QueueExplosion(
|
||||||
return;
|
args.SolutionEntity,
|
||||||
|
ExplosionType,
|
||||||
//Handle scaling
|
intensity,
|
||||||
if (_scaled)
|
IntensitySlope,
|
||||||
{
|
MaxIntensity);
|
||||||
floatIntensity = MathF.Min(floatIntensity, _maxScale);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
floatIntensity = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Calculate intensities
|
|
||||||
var finalDevastationRange = (int)MathF.Round(_devastationRange * floatIntensity);
|
|
||||||
var finalHeavyImpactRange = (int)MathF.Round(_heavyImpactRange * floatIntensity);
|
|
||||||
var finalLightImpactRange = (int)MathF.Round(_lightImpactRange * floatIntensity);
|
|
||||||
var finalFlashRange = (int)MathF.Round(_flashRange * floatIntensity);
|
|
||||||
EntitySystem.Get<ExplosionSystem>().SpawnExplosion(args.SolutionEntity, finalDevastationRange,
|
|
||||||
finalHeavyImpactRange, finalLightImpactRange, finalFlashRange);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
using Content.Server.Construction;
|
using Content.Server.Construction;
|
||||||
using Content.Server.Destructible.Thresholds;
|
using Content.Server.Destructible.Thresholds;
|
||||||
|
using Content.Server.Destructible.Thresholds.Behaviors;
|
||||||
|
using Content.Server.Destructible.Thresholds.Triggers;
|
||||||
using Content.Server.Explosion.EntitySystems;
|
using Content.Server.Explosion.EntitySystems;
|
||||||
using Content.Server.Stack;
|
using Content.Server.Stack;
|
||||||
using Content.Shared.Acts;
|
using Content.Shared.Acts;
|
||||||
using Content.Shared.Damage;
|
using Content.Shared.Damage;
|
||||||
|
using Content.Shared.FixedPoint;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Content.Server.Destructible
|
namespace Content.Server.Destructible
|
||||||
{
|
{
|
||||||
@@ -50,6 +54,40 @@ namespace Content.Server.Destructible
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FFS this shouldn't be this hard. Maybe this should just be a field of the destructible component. Its not
|
||||||
|
// like there is currently any entity that is NOT just destroyed upon reaching a total-damage value.
|
||||||
|
/// <summary>
|
||||||
|
/// Figure out how much damage an entity needs to have in order to be destroyed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This assumes that this entity has some sort of destruction or breakage behavior triggered by a
|
||||||
|
/// total-damage threshold.
|
||||||
|
/// </remarks>
|
||||||
|
public FixedPoint2 DestroyedAt(EntityUid uid, DestructibleComponent? destructible = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref destructible, logMissing: false))
|
||||||
|
return FixedPoint2.MaxValue;
|
||||||
|
|
||||||
|
// We have nested for loops here, but the vast majority of components only have one threshold with 1-3 behaviors.
|
||||||
|
// Really, this should probably just be a property of the damageable component.
|
||||||
|
var damageNeeded = FixedPoint2.MaxValue;
|
||||||
|
foreach (var threshold in destructible.Thresholds)
|
||||||
|
{
|
||||||
|
if (threshold.Trigger is not DamageTrigger trigger)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var behavior in threshold.Behaviors)
|
||||||
|
{
|
||||||
|
if (behavior is DoActsBehavior actBehavior &&
|
||||||
|
actBehavior.HasAct(ThresholdActs.Destruction | ThresholdActs.Breakage))
|
||||||
|
{
|
||||||
|
damageNeeded = Math.Min(damageNeeded.Float(), trigger.Damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return damageNeeded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently only used for destructible integration tests. Unless other uses are found for this, maybe this should just be removed and the tests redone.
|
// Currently only used for destructible integration tests. Unless other uses are found for this, maybe this should just be removed and the tests redone.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
|
|||||||
{
|
{
|
||||||
public void Execute(EntityUid owner, DestructibleSystem system)
|
public void Execute(EntityUid owner, DestructibleSystem system)
|
||||||
{
|
{
|
||||||
system.ExplosionSystem.SpawnExplosion(owner);
|
system.ExplosionSystem.TriggerExplosive(owner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
using Content.Server.Throwing;
|
|
||||||
using Content.Shared.Acts;
|
|
||||||
using Content.Shared.Throwing;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Maths;
|
|
||||||
|
|
||||||
namespace Content.Server.Explosion.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed class ExplosionLaunchedComponent : Component, IExAct
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
|
||||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
|
||||||
|
|
||||||
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (_entMan.Deleted(Owner))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var sourceLocation = eventArgs.Source;
|
|
||||||
var targetLocation = _entMan.GetComponent<TransformComponent>(eventArgs.Target).Coordinates;
|
|
||||||
|
|
||||||
if (sourceLocation.Equals(targetLocation)) return;
|
|
||||||
|
|
||||||
var offset = (targetLocation.ToMapPos(_entMan) - sourceLocation.ToMapPos(_entMan));
|
|
||||||
|
|
||||||
//Don't throw if the direction is center (0,0)
|
|
||||||
if (offset == Vector2.Zero) return;
|
|
||||||
|
|
||||||
var direction = offset.Normalized;
|
|
||||||
|
|
||||||
var throwForce = eventArgs.Severity switch
|
|
||||||
{
|
|
||||||
ExplosionSeverity.Heavy => 30,
|
|
||||||
ExplosionSeverity.Light => 20,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
_sysMan.GetEntitySystem<ThrowingSystem>().TryThrow(Owner, direction, throwForce);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Content.Server.Explosion.EntitySystems;
|
||||||
|
using Content.Shared.Explosion;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||||
|
|
||||||
|
namespace Content.Server.Explosion.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component that provides entities with explosion resistance.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is desirable over just using damage modifier sets, given that equipment like bomb-suits need to
|
||||||
|
/// significantly reduce the damage, but shouldn't be silly overpowered in regular combat.
|
||||||
|
/// </remarks>
|
||||||
|
[RegisterComponent]
|
||||||
|
[Friend(typeof(ExplosionSystem))]
|
||||||
|
public sealed class ExplosionResistanceComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The resistance values for this component, This fraction is added to the total resistance.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("resistance")]
|
||||||
|
public float GlobalResistance = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Like <see cref="GlobalResistance"/>, but specified specific to each explosion type for more customizability.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("resistances", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<float, ExplosionPrototype>))]
|
||||||
|
public Dictionary<string, float> Resistances = new();
|
||||||
|
}
|
||||||
@@ -1,34 +1,58 @@
|
|||||||
using Content.Server.Chemistry.ReactionEffects;
|
using Content.Server.Explosion.EntitySystems;
|
||||||
using Content.Server.Destructible.Thresholds.Behaviors;
|
using Content.Shared.Explosion;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
|
||||||
|
|
||||||
namespace Content.Server.Explosion.Components
|
namespace Content.Server.Explosion.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies an explosion that can be spawned by this entity. The explosion itself is spawned via <see
|
||||||
|
/// cref="ExplosionSystem.TriggerExplosive"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The total intensity may be overridden by whatever system actually calls TriggerExplosive(), but this
|
||||||
|
/// component still determines the explosion type and other properties.
|
||||||
|
/// </remarks>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class ExplosiveComponent : Component
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specifies an explosion range should this entity be exploded.
|
/// The explosion prototype. This determines the damage types, the tile-break chance, and some visual
|
||||||
|
/// information (e.g., the light that the explosion gives off).
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("explosionType", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))]
|
||||||
|
public string ExplosionType = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum intensity the explosion can have on a single tile. This limits the maximum damage and tile
|
||||||
|
/// break chance the explosion can achieve at any given location.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("maxIntensity")]
|
||||||
|
public float MaxIntensity = 4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How quickly the intensity drops off as you move away from the epicenter.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("intensitySlope")]
|
||||||
|
public float IntensitySlope = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total intensity of this explosion. The radius of the explosion scales like the cube root of this
|
||||||
|
/// number (see <see cref="ExplosionSystem.RadiusToIntensity"/>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Explosions can be caused by:
|
/// This number can be overridden by passing optional argument to <see
|
||||||
/// <list type="bullet">
|
/// cref="ExplosionSystem.TriggerExplosive"/>.
|
||||||
/// <item>Reaching a damage threshold that causes a <see cref="ExplodeBehavior"/></item>
|
|
||||||
/// <item>Being triggered via the <see cref="ExplodeOnTriggerComponent"/></item>
|
|
||||||
/// <item>Manually by some other system via functions in <see cref="ExplosionHelper"/> (for example, chemistry's
|
|
||||||
/// <see cref="ExplosionReactionEffect"/>).</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[RegisterComponent]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public sealed class ExplosiveComponent : Component
|
[DataField("totalIntensity")]
|
||||||
{
|
public float TotalIntensity = 10;
|
||||||
[DataField("devastationRange")]
|
|
||||||
public int DevastationRange;
|
|
||||||
[DataField("heavyImpactRange")]
|
|
||||||
public int HeavyImpactRange;
|
|
||||||
[DataField("lightImpactRange")]
|
|
||||||
public int LightImpactRange;
|
|
||||||
[DataField("flashRange")]
|
|
||||||
public int FlashRange;
|
|
||||||
|
|
||||||
public bool Exploding { get; set; } = false;
|
/// <summary>
|
||||||
}
|
/// Avoid somehow double-triggering this explosion (e.g. by damaging this entity from its own explosion.
|
||||||
|
/// </summary>
|
||||||
|
public bool Exploded;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
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!;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, int> _explosionTypes = new();
|
||||||
|
|
||||||
|
private void InitAirtightMap()
|
||||||
|
{
|
||||||
|
// Currently explosion prototype hot-reload isn't supported, as it would involve completely re-computing the
|
||||||
|
// airtight map. Could be done, just not yet implemented.
|
||||||
|
|
||||||
|
// for storing airtight entity damage thresholds for all anchored airtight entities, we will use integers in
|
||||||
|
// place of id-strings. This initializes the string <--> id association.
|
||||||
|
// This allows us to replace a Dictionary<string, float> with just a float[].
|
||||||
|
int index = 0;
|
||||||
|
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ExplosionPrototype>())
|
||||||
|
{
|
||||||
|
_explosionTypes.Add(prototype.ID, index);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, EntityQuery<AirtightComponent>? query = null)
|
||||||
|
{
|
||||||
|
if (_mapManager.TryGetGrid(gridId, out var grid))
|
||||||
|
UpdateAirtightMap(grid, tile, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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, EntityQuery<AirtightComponent>? query = null)
|
||||||
|
{
|
||||||
|
var tolerance = new float[_explosionTypes.Count];
|
||||||
|
var blockedDirections = AtmosDirection.Invalid;
|
||||||
|
|
||||||
|
if (!_airtightMap.ContainsKey(grid.Index))
|
||||||
|
_airtightMap[grid.Index] = new();
|
||||||
|
|
||||||
|
query ??= EntityManager.GetEntityQuery<AirtightComponent>();
|
||||||
|
var damageQuery = EntityManager.GetEntityQuery<DamageableComponent>();
|
||||||
|
var destructibleQuery = EntityManager.GetEntityQuery<DestructibleComponent>();
|
||||||
|
|
||||||
|
foreach (var uid in grid.GetAnchoredEntities(tile))
|
||||||
|
{
|
||||||
|
if (!query.Value.TryGetComponent(uid, out var airtight) || !airtight.AirBlocked)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
blockedDirections |= airtight.AirBlockedDirection;
|
||||||
|
var entityTolerances = GetExplosionTolerance(uid, damageQuery, destructibleQuery);
|
||||||
|
for (var i = 0; i < tolerance.Length; i++)
|
||||||
|
{
|
||||||
|
tolerance[i] = Math.Max(tolerance[i], entityTolerances[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 float[] GetExplosionTolerance(
|
||||||
|
EntityUid uid,
|
||||||
|
EntityQuery<DamageableComponent> damageQuery,
|
||||||
|
EntityQuery<DestructibleComponent> destructibleQuery)
|
||||||
|
{
|
||||||
|
// 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 = FixedPoint2.MaxValue;
|
||||||
|
if (destructibleQuery.TryGetComponent(uid, out var destructible))
|
||||||
|
{
|
||||||
|
totalDamageTarget = _destructibleSystem.DestroyedAt(uid, destructible);
|
||||||
|
}
|
||||||
|
|
||||||
|
var explosionTolerance = new float[_explosionTypes.Count];
|
||||||
|
if (totalDamageTarget == FixedPoint2.MaxValue || !damageQuery.TryGetComponent(uid, out var damageable))
|
||||||
|
{
|
||||||
|
for (var i = 0; i < explosionTolerance.Length; i++)
|
||||||
|
{
|
||||||
|
explosionTolerance[i] = float.MaxValue;
|
||||||
|
}
|
||||||
|
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 (id, index) in _explosionTypes)
|
||||||
|
{
|
||||||
|
if (!_prototypeManager.TryIndex<ExplosionPrototype>(id, out var explosionType))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
{
|
||||||
|
explosionTolerance[index] = float.MaxValue;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ev = new GetExplosionResistanceEvent(explosionType.ID);
|
||||||
|
RaiseLocalEvent(uid, ev, false);
|
||||||
|
|
||||||
|
damagePerIntensity += value * Math.Clamp(0, 1 - ev.Resistance, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
explosionTolerance[index] = (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return explosionTolerance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data struct that describes the explosion-blocking airtight entities on a tile.
|
||||||
|
/// </summary>
|
||||||
|
public struct TileData
|
||||||
|
{
|
||||||
|
public TileData(float[] explosionTolerance, AtmosDirection blockedDirections)
|
||||||
|
{
|
||||||
|
ExplosionTolerance = explosionTolerance;
|
||||||
|
BlockedDirections = blockedDirections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float[] ExplosionTolerance;
|
||||||
|
public AtmosDirection BlockedDirections = AtmosDirection.Invalid;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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; }
|
||||||
|
public int SingleTickAreaLimit {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);
|
||||||
|
_cfg.OnValueChanged(CCVars.ExplosionSingleTickAreaLimit, SetSingleTickAreaLimit, 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);
|
||||||
|
_cfg.UnsubValueChanged(CCVars.ExplosionSingleTickAreaLimit, SetSingleTickAreaLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
private void SetSingleTickAreaLimit(int value) => SingleTickAreaLimit = value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
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, NeighborFlag>> _gridEdges = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On grid startup, prepare a map of grid edges.
|
||||||
|
/// </summary>
|
||||||
|
private void OnGridStartup(GridStartupEvent ev)
|
||||||
|
{
|
||||||
|
var grid = _mapManager.GetGrid(ev.GridId);
|
||||||
|
|
||||||
|
Dictionary<Vector2i, NeighborFlag> edges = new();
|
||||||
|
_gridEdges[ev.GridId] = edges;
|
||||||
|
|
||||||
|
foreach (var tileRef in grid.GetAllTiles())
|
||||||
|
{
|
||||||
|
if (IsEdge(grid, tileRef.GridIndices, out var dir))
|
||||||
|
edges.Add(tileRef.GridIndices, dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGridRemoved(GridRemovalEvent ev)
|
||||||
|
{
|
||||||
|
_airtightMap.Remove(ev.GridId);
|
||||||
|
_gridEdges.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, BlockedSpaceTile>, ushort) TransformGridEdges(
|
||||||
|
MapCoordinates epicentre,
|
||||||
|
GridId? referenceGrid,
|
||||||
|
List<GridId> localGrids,
|
||||||
|
float maxDistance)
|
||||||
|
{
|
||||||
|
Dictionary<Vector2i, BlockedSpaceTile> transformedEdges = new();
|
||||||
|
|
||||||
|
var targetMatrix = Matrix3.Identity;
|
||||||
|
Angle targetAngle = new();
|
||||||
|
var tileSize = DefaultTileSize;
|
||||||
|
var maxDistanceSq = (int) (maxDistance * maxDistance);
|
||||||
|
|
||||||
|
// 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 / 2f;
|
||||||
|
offsetMatrix.R1C2 = tileSize / 2f;
|
||||||
|
|
||||||
|
// Here we can end up with 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)
|
||||||
|
|
||||||
|
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))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (grid.TileSize != tileSize)
|
||||||
|
{
|
||||||
|
Logger.Error($"Explosions do not support grids with different grid sizes. GridIds: {gridToTransform} and {referenceGrid}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
|
||||||
|
var xform = xforms.GetComponent(grid.GridEntityId);
|
||||||
|
var (_, gridWorldRotation, gridWorldMatrix, invGridWorldMatrid) = xform.GetWorldPositionRotationMatrixWithInv(xforms);
|
||||||
|
|
||||||
|
var localEpicentre = (Vector2i) invGridWorldMatrid.Transform(epicentre.Position);
|
||||||
|
var matrix = offsetMatrix * gridWorldMatrix * targetMatrix;
|
||||||
|
var angle = gridWorldRotation - targetAngle;
|
||||||
|
|
||||||
|
var (x, y) = angle.RotateVec((tileSize / 4f, tileSize / 4f));
|
||||||
|
|
||||||
|
foreach (var (tile, dir) in edges)
|
||||||
|
{
|
||||||
|
// if a tile is further than max distance from the epicentre, we just ignore it.
|
||||||
|
var delta = tile - localEpicentre;
|
||||||
|
if (delta.X * delta.X + delta.Y * delta.Y > maxDistanceSq) // no Vector2.Length???
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var center = matrix.Transform(tile);
|
||||||
|
|
||||||
|
if ((dir & NeighborFlag.Cardinal) == 0)
|
||||||
|
{
|
||||||
|
// this is purely a diagonal edge tile
|
||||||
|
var newIndex = new Vector2i((int) MathF.Floor(center.X), (int) MathF.Floor(center.Y));
|
||||||
|
if (!transformedEdges.TryGetValue(newIndex, out var data))
|
||||||
|
{
|
||||||
|
data = new();
|
||||||
|
transformedEdges[newIndex] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.BlockingGridEdges.Add(new(default, null, center, angle, tileSize));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of just mapping the center of the tile, we map for points on that tile. This is basically a
|
||||||
|
// shitty approximation to doing a proper check to get all space-tiles that intersect this grid tile.
|
||||||
|
// Not perfect, but works well enough.
|
||||||
|
|
||||||
|
HashSet<Vector2i> transformedTiles = new()
|
||||||
|
{
|
||||||
|
new((int) MathF.Floor(center.X + x), (int) MathF.Floor(center.Y + x)), // center of tile, offset by (0.25, 0.25) in tile coordinates
|
||||||
|
new((int) MathF.Floor(center.X - y), (int) MathF.Floor(center.Y - y)), // center offset by (-0.25, 0.25)
|
||||||
|
new((int) MathF.Floor(center.X - x), (int) MathF.Floor(center.Y + y)), // offset by (-0.25, -0.25)
|
||||||
|
new((int) MathF.Floor(center.X + y), (int) MathF.Floor(center.Y - x)), // offset by (0.25, -0.25)
|
||||||
|
};
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, dir) in localEdges)
|
||||||
|
{
|
||||||
|
// grids cannot overlap, so tile should never be an existing entry.
|
||||||
|
// if this ever changes, this needs to do a try-get.
|
||||||
|
var data = new BlockedSpaceTile();
|
||||||
|
transformedEdges[tile] = data;
|
||||||
|
|
||||||
|
data.UnblockedDirections = AtmosDirection.Invalid; // all directions are blocked automatically.
|
||||||
|
|
||||||
|
if ((dir & NeighborFlag.Cardinal) == 0)
|
||||||
|
data.BlockingGridEdges.Add(new(default, null, ((Vector2) tile + 0.5f) * tileSize, 0, tileSize));
|
||||||
|
else
|
||||||
|
data.BlockingGridEdges.Add(new(tile, referenceGrid.Value, ((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, BlockedSpaceTile> transformedEdges, float 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 / 2f)))
|
||||||
|
data.UnblockedDirections &= ~AtmosDirection.North;
|
||||||
|
|
||||||
|
// check south
|
||||||
|
if (edge.Box.Contains(tileCenter + (0, -tileSize / 2f)))
|
||||||
|
data.UnblockedDirections &= ~AtmosDirection.South;
|
||||||
|
|
||||||
|
// check east
|
||||||
|
if (edge.Box.Contains(tileCenter + (tileSize / 2f, 0)))
|
||||||
|
data.UnblockedDirections &= ~AtmosDirection.East;
|
||||||
|
|
||||||
|
// check west
|
||||||
|
if (edge.Box.Contains(tileCenter + (-tileSize / 2f, 0)))
|
||||||
|
data.UnblockedDirections &= ~AtmosDirection.West;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When a tile is updated, we might need to update the grid edge maps.
|
||||||
|
/// </summary>
|
||||||
|
private void OnTileChanged(TileChangedEvent ev)
|
||||||
|
{
|
||||||
|
// only need to update the grid-edge map if a tile was added or removed from the grid.
|
||||||
|
if (!ev.NewTile.Tile.IsEmpty && !ev.OldTile.IsEmpty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var tileRef = ev.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 (tileRef.Tile.IsEmpty)
|
||||||
|
{
|
||||||
|
// if the tile is empty, it cannot itself be an edge tile.
|
||||||
|
edges.Remove(tileRef.GridIndices);
|
||||||
|
|
||||||
|
// add any valid neighbours to the list of edge-tiles
|
||||||
|
for (var i = 0; i < NeighbourVectors.Length; i++)
|
||||||
|
{
|
||||||
|
var neighbourIndex = tileRef.GridIndices + NeighbourVectors[i];
|
||||||
|
|
||||||
|
if (grid.TryGetTileRef(neighbourIndex, out var neighbourTile) && !neighbourTile.Tile.IsEmpty)
|
||||||
|
{
|
||||||
|
var oppositeDirection = (NeighborFlag) (1 << ((i + 4) % 8));
|
||||||
|
edges[neighbourIndex] = edges.GetValueOrDefault(neighbourIndex) | oppositeDirection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the tile is not empty space, but was previously. So update directly adjacent neighbours, which may no longer
|
||||||
|
// be edge tiles.
|
||||||
|
for (var i = 0; i < NeighbourVectors.Length; i++)
|
||||||
|
{
|
||||||
|
var neighbourIndex = tileRef.GridIndices + NeighbourVectors[i];
|
||||||
|
|
||||||
|
if (edges.TryGetValue(neighbourIndex, out var neighborSpaceDir))
|
||||||
|
{
|
||||||
|
var oppositeDirection = (NeighborFlag) (1 << ((i + 4) % 8));
|
||||||
|
neighborSpaceDir &= ~oppositeDirection;
|
||||||
|
if (neighborSpaceDir == NeighborFlag.Invalid)
|
||||||
|
{
|
||||||
|
// no longer an edge tile
|
||||||
|
edges.Remove(neighbourIndex);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
edges[neighbourIndex] = neighborSpaceDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally check if the new tile is itself an edge tile
|
||||||
|
if (IsEdge(grid, tileRef.GridIndices, out var spaceDir))
|
||||||
|
edges.Add(tileRef.GridIndices, spaceDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 NeighborFlag spaceDirections)
|
||||||
|
{
|
||||||
|
spaceDirections = NeighborFlag.Invalid;
|
||||||
|
for (var i = 0; i < NeighbourVectors.Length; i++)
|
||||||
|
{
|
||||||
|
if (!grid.TryGetTileRef(index + NeighbourVectors[i], out var neighborTile) || neighborTile.Tile.IsEmpty)
|
||||||
|
spaceDirections |= (NeighborFlag) (1 << i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spaceDirections != NeighborFlag.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// yeah this is now the third direction flag enum, and the 5th (afaik) direction enum overall.....
|
||||||
|
/// <summary>
|
||||||
|
/// Directional bitflags used to denote the neighbouring tiles of some tile on a grid.. Differ from atmos and
|
||||||
|
/// normal directional flags as NorthEast != North | East
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum NeighborFlag : byte
|
||||||
|
{
|
||||||
|
Invalid = 0,
|
||||||
|
North = 1 << 0,
|
||||||
|
NorthEast = 1 << 1,
|
||||||
|
East = 1 << 2,
|
||||||
|
SouthEast = 1 << 3,
|
||||||
|
South = 1 << 4,
|
||||||
|
SouthWest = 1 << 5,
|
||||||
|
West = 1 << 6,
|
||||||
|
NorthWest = 1 << 7,
|
||||||
|
|
||||||
|
Cardinal = North | East | South | West,
|
||||||
|
Diagonal = NorthEast | SouthEast | SouthWest | NorthWest,
|
||||||
|
Any = Cardinal | Diagonal
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool AnyNeighborBlocked(NeighborFlag neighbors, AtmosDirection blockedDirs)
|
||||||
|
{
|
||||||
|
if ((neighbors & NeighborFlag.North) == NeighborFlag.North && (blockedDirs & AtmosDirection.North) == AtmosDirection.North)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if ((neighbors & NeighborFlag.South) == NeighborFlag.South && (blockedDirs & AtmosDirection.South) == AtmosDirection.South)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if ((neighbors & NeighborFlag.East) == NeighborFlag.East && (blockedDirs & AtmosDirection.East) == AtmosDirection.East)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if ((neighbors & NeighborFlag.West) == NeighborFlag.West && (blockedDirs & AtmosDirection.West) == AtmosDirection.West)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// array indices match NeighborFlags shifts.
|
||||||
|
public static readonly Vector2i[] NeighbourVectors =
|
||||||
|
{
|
||||||
|
new (0, 1),
|
||||||
|
new (1, 1),
|
||||||
|
new (1, 0),
|
||||||
|
new (1, -1),
|
||||||
|
new (0, -1),
|
||||||
|
new (-1, -1),
|
||||||
|
new (-1, 0),
|
||||||
|
new (-1, 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This class has information about the space equivalent of an airtight entity blocking explosions: the edges of grids.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BlockedSpaceTile
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// What directions of this tile are not blocked?
|
||||||
|
/// </summary>
|
||||||
|
public AtmosDirection UnblockedDirections = AtmosDirection.All;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The set of grid edge-tiles that are blocking this space tile.
|
||||||
|
/// </summary>
|
||||||
|
public List<GridEdgeData> BlockingGridEdges = new();
|
||||||
|
|
||||||
|
public sealed class 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,642 @@
|
|||||||
|
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;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
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 int _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>
|
||||||
|
/// 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 (_activeExplosion.Area > SingleTickAreaLimit)
|
||||||
|
break; // start processing next turn.
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO EXPLOSION check if active explosion is on a paused map. If it is... I guess support swapping out &
|
||||||
|
// storing the "currently active" explosion?
|
||||||
|
|
||||||
|
var processed = _activeExplosion.Process(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, EntityQuery<PhysicsComponent> physicsQuery)
|
||||||
|
{
|
||||||
|
if (EntityManager.IsQueuedForDeletion(uid))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!physicsQuery.TryGetComponent(uid, out var physics))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return physics.CanCollide && physics.Hard && (physics.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 throwForce,
|
||||||
|
DamageSpecifier damage,
|
||||||
|
MapCoordinates epicenter,
|
||||||
|
HashSet<EntityUid> processed,
|
||||||
|
string id,
|
||||||
|
EntityQuery<TransformComponent> xformQuery,
|
||||||
|
EntityQuery<DamageableComponent> damageQuery,
|
||||||
|
EntityQuery<PhysicsComponent> physicsQuery,
|
||||||
|
EntityQuery<MetaDataComponent> metaQuery)
|
||||||
|
{
|
||||||
|
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, TransformComponent?) > list = new();
|
||||||
|
|
||||||
|
EntityUidQueryCallback callback = uid =>
|
||||||
|
{
|
||||||
|
if (processed.Contains(uid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!xformQuery.TryGetComponent(uid, out var xform))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (xform.ParentUid != grid.GridEntityId)
|
||||||
|
{
|
||||||
|
if (!metaQuery.TryGetComponent(uid, out var meta))
|
||||||
|
return;
|
||||||
|
// Not parented to grid. Likely in a container.
|
||||||
|
if (_containerSystem.IsEntityInContainer(uid, meta))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add((uid, xform));
|
||||||
|
};
|
||||||
|
|
||||||
|
_entityLookup.FastEntitiesIntersecting(lookup, ref gridBox, callback);
|
||||||
|
|
||||||
|
// process those entities
|
||||||
|
foreach (var (entity, xform) in list)
|
||||||
|
{
|
||||||
|
processed.Add(entity);
|
||||||
|
ProcessEntity(entity, epicenter, damage, throwForce, id, damageQuery, physicsQuery, xform);
|
||||||
|
}
|
||||||
|
|
||||||
|
// process anchored entities
|
||||||
|
var tileBlocked = false;
|
||||||
|
foreach (var entity in grid.GetAnchoredEntities(tile).ToList())
|
||||||
|
{
|
||||||
|
processed.Add(entity);
|
||||||
|
ProcessEntity(entity, epicenter, damage, throwForce, id, damageQuery, physicsQuery);
|
||||||
|
tileBlocked |= IsBlockingTurf(entity, physicsQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// And throwing is disabled for nukes anyways.
|
||||||
|
if (throwForce <= 0)
|
||||||
|
return !tileBlocked;
|
||||||
|
|
||||||
|
list.Clear();
|
||||||
|
_entityLookup.FastEntitiesIntersecting(lookup, ref gridBox, callback);
|
||||||
|
|
||||||
|
foreach (var (entity, xform) in list)
|
||||||
|
{
|
||||||
|
// Here we only throw, no dealing damage. Containers n such might drop their entities after being destroyed, but
|
||||||
|
// they should handle their own damage pass-through, with their own damage reduction calculation.
|
||||||
|
ProcessEntity(entity, epicenter, null, throwForce, id, damageQuery, physicsQuery, xform);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !tileBlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Same as <see cref="ExplodeTile"/>, but for SPAAAAAAACE.
|
||||||
|
/// </summary>
|
||||||
|
internal void ExplodeSpace(EntityLookupComponent lookup,
|
||||||
|
Matrix3 spaceMatrix,
|
||||||
|
Matrix3 invSpaceMatrix,
|
||||||
|
Vector2i tile,
|
||||||
|
float throwForce,
|
||||||
|
DamageSpecifier damage,
|
||||||
|
MapCoordinates epicenter,
|
||||||
|
HashSet<EntityUid> processed,
|
||||||
|
string id,
|
||||||
|
EntityQuery<TransformComponent> xformQuery,
|
||||||
|
EntityQuery<DamageableComponent> damageQuery,
|
||||||
|
EntityQuery<PhysicsComponent> physicsQuery,
|
||||||
|
EntityQuery<MetaDataComponent> metaQuery)
|
||||||
|
{
|
||||||
|
var gridBox = new Box2(tile * DefaultTileSize, (DefaultTileSize, DefaultTileSize));
|
||||||
|
var worldBox = spaceMatrix.TransformBox(gridBox);
|
||||||
|
List<(EntityUid, TransformComponent)> list = new();
|
||||||
|
|
||||||
|
EntityUidQueryCallback callback = uid =>
|
||||||
|
{
|
||||||
|
if (processed.Contains(uid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var xform = xformQuery.GetComponent(uid);
|
||||||
|
|
||||||
|
if (xform.ParentUid == lookup.Owner)
|
||||||
|
{
|
||||||
|
// parented directly to the map, use local position
|
||||||
|
if (gridBox.Contains(invSpaceMatrix.Transform(xform.LocalPosition)))
|
||||||
|
list.Add((uid, xform));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!metaQuery.TryGetComponent(uid, out var meta))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Not parented to map. Likely in a container.
|
||||||
|
if (_containerSystem.IsEntityInContainer(uid, meta))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// finally check if it intersects our tile
|
||||||
|
if (gridBox.Contains(invSpaceMatrix.Transform(xform.LocalPosition)))
|
||||||
|
list.Add((uid, xform));
|
||||||
|
};
|
||||||
|
|
||||||
|
_entityLookup.FastEntitiesIntersecting(lookup, ref worldBox, callback);
|
||||||
|
|
||||||
|
foreach (var (entity, xform) in list)
|
||||||
|
{
|
||||||
|
processed.Add(entity);
|
||||||
|
ProcessEntity(entity, epicenter, damage, throwForce, id, damageQuery, physicsQuery, xform);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (throwForce <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Also, throw any entities that were spawned as shrapnel. Compared to entity spawning & destruction, this extra
|
||||||
|
// lookup is relatively minor computational cost, and throwing is disabled for nukes anyways.
|
||||||
|
list.Clear();
|
||||||
|
_entityLookup.FastEntitiesIntersecting(lookup, ref worldBox, callback);
|
||||||
|
foreach (var (entity, xform) in list)
|
||||||
|
{
|
||||||
|
ProcessEntity(entity, epicenter, null, throwForce, id, damageQuery, physicsQuery, xform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This function actually applies the explosion affects to an entity.
|
||||||
|
/// </summary>
|
||||||
|
private void ProcessEntity(
|
||||||
|
EntityUid uid,
|
||||||
|
MapCoordinates epicenter,
|
||||||
|
DamageSpecifier? damage,
|
||||||
|
float throwForce,
|
||||||
|
string id,
|
||||||
|
EntityQuery<DamageableComponent> damageQuery,
|
||||||
|
EntityQuery<PhysicsComponent> physicsQuery,
|
||||||
|
TransformComponent? xform = null)
|
||||||
|
{
|
||||||
|
// damage
|
||||||
|
if (damage != null && damageQuery.TryGetComponent(uid, out var damageable))
|
||||||
|
{
|
||||||
|
var ev = new GetExplosionResistanceEvent(id);
|
||||||
|
RaiseLocalEvent(uid, ev, false);
|
||||||
|
|
||||||
|
if (ev.Resistance == 0)
|
||||||
|
{
|
||||||
|
// no damage-dict multiplication required.
|
||||||
|
_damageableSystem.TryChangeDamage(uid, damage, ignoreResistances: true, damageable: damageable);
|
||||||
|
}
|
||||||
|
else if (ev.Resistance < 1)
|
||||||
|
{
|
||||||
|
_damageableSystem.TryChangeDamage(uid, damage * (1 - ev.Resistance), ignoreResistances: true, damageable: damageable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// throw
|
||||||
|
if (xform != null
|
||||||
|
&& !xform.Anchored
|
||||||
|
&& throwForce > 0
|
||||||
|
&& !EntityManager.IsQueuedForDeletion(uid)
|
||||||
|
&& physicsQuery.TryGetComponent(uid, out var physics)
|
||||||
|
&& physics.BodyType == BodyType.Dynamic)
|
||||||
|
{
|
||||||
|
// TODO purge throw helpers and pass in physics component
|
||||||
|
_throwingSystem.TryThrow(uid, xform.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();
|
||||||
|
|
||||||
|
private EntityQuery<TransformComponent> _xformQuery;
|
||||||
|
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||||
|
private EntityQuery<DamageableComponent> _damageQuery;
|
||||||
|
private EntityQuery<MetaDataComponent> _metaQuery;
|
||||||
|
|
||||||
|
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,
|
||||||
|
IEntityManager entMan,
|
||||||
|
IMapManager mapMan)
|
||||||
|
{
|
||||||
|
_system = system;
|
||||||
|
ExplosionType = explosionType;
|
||||||
|
_tileSetIntensity = tileSetIntensity;
|
||||||
|
Epicenter = epicenter;
|
||||||
|
Area = area;
|
||||||
|
|
||||||
|
_xformQuery = entMan.GetEntityQuery<TransformComponent>();
|
||||||
|
_physicsQuery = entMan.GetEntityQuery<PhysicsComponent>();
|
||||||
|
_damageQuery = entMan.GetEntityQuery<DamageableComponent>();
|
||||||
|
_metaQuery = entMan.GetEntityQuery<MetaDataComponent>();
|
||||||
|
|
||||||
|
if (spaceData != null)
|
||||||
|
{
|
||||||
|
var mapUid = mapMan.GetMapEntityId(epicenter.MapId);
|
||||||
|
|
||||||
|
_explosionData.Add(new()
|
||||||
|
{
|
||||||
|
TileLists = spaceData.TileLists,
|
||||||
|
Lookup = entMan.GetComponent<EntityLookupComponent>(mapUid),
|
||||||
|
MapGrid = null
|
||||||
|
});
|
||||||
|
|
||||||
|
_spaceMatrix = spaceMatrix;
|
||||||
|
_invSpaceMatrix = Matrix3.Invert(spaceMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var grid in gridData)
|
||||||
|
{
|
||||||
|
_explosionData.Add(new()
|
||||||
|
{
|
||||||
|
TileLists = grid.TileLists,
|
||||||
|
Lookup = entMan.GetComponent<EntityLookupComponent>(grid.Grid.GridEntityId),
|
||||||
|
MapGrid = grid.Grid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetNextTileEnumerator())
|
||||||
|
MoveNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetNextTileEnumerator()
|
||||||
|
{
|
||||||
|
while (CurrentIteration < _tileSetIntensity.Count)
|
||||||
|
{
|
||||||
|
_currentIntensity = _tileSetIntensity[CurrentIteration];
|
||||||
|
_currentDamage = ExplosionType.DamagePerIntensity * _currentIntensity;
|
||||||
|
|
||||||
|
// only throw if either the explosion is small, or if this is the outer ring of a large explosion.
|
||||||
|
var doThrow = Area < _system.ThrowLimit || CurrentIteration > _tileSetIntensity.Count - 6;
|
||||||
|
_currentThrowForce = doThrow ? 10 * MathF.Sqrt(_currentIntensity) : 0;
|
||||||
|
|
||||||
|
// 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 Process(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,
|
||||||
|
_currentThrowForce,
|
||||||
|
_currentDamage,
|
||||||
|
Epicenter,
|
||||||
|
ProcessedEntities,
|
||||||
|
ExplosionType.ID,
|
||||||
|
_xformQuery,
|
||||||
|
_damageQuery,
|
||||||
|
_physicsQuery,
|
||||||
|
_metaQuery);
|
||||||
|
|
||||||
|
// was there a blocking entity on the tile that was not destroyed by the explosion?
|
||||||
|
if (canDamageFloor)
|
||||||
|
_system.DamageFloorTile(tileRef, _currentIntensity, tileUpdateList, ExplosionType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_system.ExplodeSpace(_currentLookup,
|
||||||
|
_spaceMatrix,
|
||||||
|
_invSpaceMatrix,
|
||||||
|
_currentEnumerator.Current,
|
||||||
|
_currentThrowForce,
|
||||||
|
_currentDamage,
|
||||||
|
Epicenter,
|
||||||
|
ProcessedEntities,
|
||||||
|
ExplosionType.ID,
|
||||||
|
_xformQuery,
|
||||||
|
_damageQuery,
|
||||||
|
_physicsQuery,
|
||||||
|
_metaQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,337 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
if (!_explosionTypes.TryGetValue(typeID, out var typeIndex))
|
||||||
|
{
|
||||||
|
Logger.Error("Attempted to spawn explosion using a prototype that was not defined during initialization. Explosion prototype hot-reload is not currently supported.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2i initialTile;
|
||||||
|
GridId? epicentreGrid = null;
|
||||||
|
var (localGrids, referenceGrid, maxDistance) = 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,
|
||||||
|
typeIndex,
|
||||||
|
_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, referenceGrid, localGrids, maxDistance);
|
||||||
|
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 = 1;
|
||||||
|
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,
|
||||||
|
typeIndex,
|
||||||
|
_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, referenceGrid, localGrids, maxDistance);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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?, float) 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 included 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.
|
||||||
|
|
||||||
|
radius *= 4;
|
||||||
|
box = Box2.CenteredAround(epicenter.Position, (radius, radius));
|
||||||
|
var mapGrids = _mapManager.FindGridsIntersecting(epicenter.MapId, box).ToList();
|
||||||
|
var grids = mapGrids.Select(x => x.Index).ToList();
|
||||||
|
|
||||||
|
if (referenceGrid != null)
|
||||||
|
return (grids, referenceGrid, radius);
|
||||||
|
|
||||||
|
// 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, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,313 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.Administration.Logs;
|
using Content.Server.Administration.Logs;
|
||||||
|
using Content.Server.Atmos.Components;
|
||||||
using Content.Server.Explosion.Components;
|
using Content.Server.Explosion.Components;
|
||||||
using Content.Shared.Acts;
|
using Content.Server.NodeContainer.EntitySystems;
|
||||||
using Content.Shared.Camera;
|
using Content.Shared.Camera;
|
||||||
|
using Content.Shared.Damage;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Explosion;
|
||||||
using Content.Shared.Interaction.Helpers;
|
using Content.Shared.Throwing;
|
||||||
using Content.Shared.Maps;
|
using Robust.Server.Containers;
|
||||||
using Content.Shared.Physics;
|
using Robust.Server.Player;
|
||||||
using Content.Shared.Sound;
|
|
||||||
using Content.Shared.Tag;
|
|
||||||
using Robust.Server.GameObjects;
|
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Maths;
|
|
||||||
using Robust.Shared.Physics;
|
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
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 SharedInteractionSystem _interactionSystem = default!;
|
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
/// <summary>
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
/// Distance used for camera shake when distance from explosion is (0.0, 0.0).
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
/// Avoids getting NaN values down the line from doing math on (0.0, 0.0).
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
/// </summary>
|
|
||||||
private static readonly Vector2 EpicenterDistance = (0.1f, 0.1f);
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
|
|
||||||
// TODO move this to the component
|
|
||||||
private static readonly SoundSpecifier ExplosionSound = new SoundCollectionSpecifier("explosion");
|
|
||||||
|
|
||||||
|
[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!;
|
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
|
||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
[Dependency] private readonly AdminLogSystem _logsSystem = default!;
|
||||||
[Dependency] private readonly IMapManager _maps = default!;
|
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
|
||||||
[Dependency] private readonly ITileDefinitionManager _tiles = default!;
|
|
||||||
|
|
||||||
[Dependency] private readonly ActSystem _acts = default!;
|
/// <summary>
|
||||||
[Dependency] private readonly EffectSystem _effects = default!;
|
/// "Tile-size" for space when there are no nearby grids to use as a reference.
|
||||||
[Dependency] private readonly TriggerSystem _triggers = default!;
|
/// </summary>
|
||||||
[Dependency] private readonly AdminLogSystem _logSystem = default!;
|
public const ushort DefaultTileSize = 1;
|
||||||
[Dependency] private readonly CameraRecoilSystem _cameraRecoil = default!;
|
|
||||||
[Dependency] private readonly TagSystem _tags = default!;
|
|
||||||
|
|
||||||
private bool IgnoreExplosivePassable(EntityUid e)
|
private AudioParams _audioParams = AudioParams.Default.WithVolume(-3f);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The "default" explosion prototype.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Generally components should specify an explosion prototype via a yaml datafield, so that the yaml-linter can
|
||||||
|
/// find errors. However some components, like rogue arrows, or some commands like the admin-smite need to have
|
||||||
|
/// a "default" option specified outside of yaml data-fields. Hence this const string.
|
||||||
|
/// </remarks>
|
||||||
|
public const string DefaultExplosionPrototypeId = "Default";
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
return _tags.HasTag(e, "ExplosivePassable");
|
base.Initialize();
|
||||||
|
|
||||||
|
DebugTools.Assert(_prototypeManager.HasIndex<ExplosionPrototype>(DefaultExplosionPrototypeId));
|
||||||
|
|
||||||
|
// handled in ExplosionSystemGridMap.cs
|
||||||
|
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
|
||||||
|
SubscribeLocalEvent<GridStartupEvent>(OnGridStartup);
|
||||||
|
SubscribeLocalEvent<ExplosionResistanceComponent, GetExplosionResistanceEvent>(OnGetResistance);
|
||||||
|
SubscribeLocalEvent<TileChangedEvent>(OnTileChanged);
|
||||||
|
|
||||||
|
// handled in ExplosionSystemAirtight.cs
|
||||||
|
SubscribeLocalEvent<AirtightComponent, DamageChangedEvent>(OnAirtightDamaged);
|
||||||
|
SubscribeCvars();
|
||||||
|
InitAirtightMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExplosionSeverity CalculateSeverity(float distance, float devastationRange, float heavyRange)
|
public override void Shutdown()
|
||||||
{
|
{
|
||||||
if (distance < devastationRange)
|
base.Shutdown();
|
||||||
{
|
UnsubscribeCvars();
|
||||||
return ExplosionSeverity.Destruction;
|
|
||||||
}
|
|
||||||
else if (distance < heavyRange)
|
|
||||||
{
|
|
||||||
return ExplosionSeverity.Heavy;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return ExplosionSeverity.Light;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CameraShakeInRange(EntityCoordinates epicenter, float maxRange)
|
private void OnGetResistance(EntityUid uid, ExplosionResistanceComponent component, GetExplosionResistanceEvent args)
|
||||||
{
|
{
|
||||||
var players = Filter.Empty()
|
args.Resistance += component.GlobalResistance;
|
||||||
.AddInRange(epicenter.ToMap(EntityManager), MathF.Ceiling(maxRange))
|
if (component.Resistances.TryGetValue(args.ExplotionPrototype, out var resistance))
|
||||||
.Recipients;
|
args.Resistance += resistance;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Damage entities inside the range. The damage depends on a discrete
|
/// Given an entity with an explosive component, spawn the appropriate explosion.
|
||||||
/// damage bracket [light, heavy, devastation] and the distance from the epicenter
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>
|
/// <remarks>
|
||||||
/// A dictionary of coordinates relative to the parents of every grid of entities that survived the explosion,
|
/// Also accepts radius or intensity arguments. This is useful for explosives where the intensity is not
|
||||||
/// have an airtight component and are currently blocking air. Like a wall.
|
/// specified in the yaml / by the component, but determined dynamically (e.g., by the quantity of a
|
||||||
/// </returns>
|
/// solution in a reaction).
|
||||||
private void DamageEntitiesInRange(
|
/// </remarks>
|
||||||
EntityCoordinates epicenter,
|
public void TriggerExplosive(EntityUid uid, ExplosiveComponent? explosive = null, bool delete = true, float? totalIntensity = null, float? radius = null, EntityUid? user = null)
|
||||||
Box2 boundingBox,
|
|
||||||
float devastationRange,
|
|
||||||
float heavyRange,
|
|
||||||
float maxRange,
|
|
||||||
MapId mapId)
|
|
||||||
{
|
{
|
||||||
var entitiesInRange = _entityLookup.GetEntitiesInRange(mapId, boundingBox, 0).ToList();
|
// 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;
|
||||||
|
|
||||||
var impassableEntities = new List<(EntityUid, float)>();
|
// No reusable explosions here.
|
||||||
var nonImpassableEntities = new List<(EntityUid, float)>();
|
if (explosive.Exploded)
|
||||||
// TODO: Given this seems to rely on physics it should just query directly like everything else.
|
return;
|
||||||
|
|
||||||
// The entities are paired with their distance to the epicenter
|
explosive.Exploded = true;
|
||||||
// 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 (!EntityManager.GetComponent<TransformComponent>(entity).Coordinates.TryDistance(EntityManager, epicenter, out var distance) ||
|
// Override the explosion intensity if optional arguments were provided.
|
||||||
distance > maxRange)
|
if (radius != null)
|
||||||
{
|
totalIntensity ??= RadiusToIntensity((float) radius, explosive.IntensitySlope, explosive.MaxIntensity);
|
||||||
continue;
|
totalIntensity ??= explosive.TotalIntensity;
|
||||||
}
|
|
||||||
|
|
||||||
if (!EntityManager.TryGetComponent(entity, out FixturesComponent? fixturesComp) || fixturesComp.Fixtures.Count < 1)
|
QueueExplosion(uid,
|
||||||
{
|
explosive.ExplosionType,
|
||||||
continue;
|
(float) totalIntensity,
|
||||||
}
|
explosive.IntensitySlope,
|
||||||
|
explosive.MaxIntensity,
|
||||||
|
user);
|
||||||
|
|
||||||
if (!EntityManager.TryGetComponent(entity, out PhysicsComponent? body))
|
if (delete)
|
||||||
{
|
EntityManager.QueueDeleteEntity(uid);
|
||||||
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>
|
/// <summary>
|
||||||
/// Damage tiles inside the range. The type of tile can change depending on a discrete
|
/// 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".
|
||||||
/// damage bracket [light, heavy, devastation], the distance from the epicenter and
|
|
||||||
/// a probability bracket [<see cref="LightBreakChance"/>, <see cref="HeavyBreakChance"/>, 1.0].
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
///
|
/// <remarks>
|
||||||
private void DamageTilesInRange(EntityCoordinates epicenter,
|
/// This assumes the explosion is in a vacuum / unobstructed. Given that explosions are not perfectly
|
||||||
GridId gridId,
|
/// circular, here radius actually means the sqrt(Area/pi), where the area is the total number of tiles
|
||||||
Box2 boundingBox,
|
/// covered by the explosion. Until you get to radius 30+, this is functionally equivalent to the
|
||||||
float devastationRange,
|
/// actual radius.
|
||||||
float heaveyRange,
|
/// </remarks>
|
||||||
float maxRange)
|
public float RadiusToIntensity(float radius, float slope, float maxIntensity = 0)
|
||||||
{
|
{
|
||||||
if (!_maps.TryGetGrid(gridId, out var mapGrid))
|
// 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
|
||||||
return;
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EntityManager.EntityExists(mapGrid.GridEntityId))
|
/// <summary>
|
||||||
|
/// Inverse formula for <see cref="RadiusToIntensity"/>
|
||||||
|
/// </summary>
|
||||||
|
public float IntensityToRadius(float totalIntensity, float slope, float maxIntensity)
|
||||||
{
|
{
|
||||||
return;
|
// max radius to avoid being capped by max-intensity
|
||||||
|
var r0 = maxIntensity / slope;
|
||||||
|
|
||||||
|
// volume at r0
|
||||||
|
var v0 = RadiusToIntensity(r0, slope);
|
||||||
|
|
||||||
|
if (totalIntensity <= v0)
|
||||||
|
{
|
||||||
|
// maxIntensity is a non-issue, can use simple inverse formula
|
||||||
|
return MathF.Cbrt(3 * totalIntensity / (slope * MathF.PI));
|
||||||
}
|
}
|
||||||
|
|
||||||
var tilesInGridAndCircle = mapGrid.GetTilesIntersecting(boundingBox);
|
return r0 * (MathF.Sqrt(12 * totalIntensity/ v0 - 3) / 6 + 0.5f);
|
||||||
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))
|
/// <summary>
|
||||||
{
|
/// Queue an explosions, centered on some entity.
|
||||||
continue;
|
/// </summary>
|
||||||
}
|
public void QueueExplosion(EntityUid uid,
|
||||||
|
string typeId,
|
||||||
if (!_interactionSystem.InRangeUnobstructed(tileLoc.ToMap(EntityManager), epicenterMapPos, maxRange, predicate: IgnoreExplosivePassable))
|
float totalIntensity,
|
||||||
{
|
float slope,
|
||||||
continue;
|
float maxTileIntensity,
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
EntityUid? user = null,
|
||||||
ExplosiveComponent? explosive = null,
|
bool addLog = false)
|
||||||
TransformComponent? transform = null)
|
|
||||||
{
|
|
||||||
if (!Resolve(entity, ref transform))
|
|
||||||
{
|
{
|
||||||
|
var pos = Transform(uid).MapPosition;
|
||||||
|
|
||||||
|
|
||||||
|
QueueExplosion(pos, typeId, totalIntensity, slope, maxTileIntensity, addLog: false);
|
||||||
|
|
||||||
|
if (!addLog)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
Resolve(entity, ref explosive, false);
|
if (user == null)
|
||||||
|
_logsSystem.Add(LogType.Explosion, LogImpact.High,
|
||||||
if (explosive is { Exploding: false })
|
$"{ToPrettyString(uid):entity} exploded at {pos:coordinates} with intensity {totalIntensity} slope {slope}");
|
||||||
{
|
|
||||||
_triggers.Explode(entity, explosive, user);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
_logsSystem.Add(LogType.Explosion, LogImpact.High,
|
||||||
while (EntityManager.EntityExists(entity) && entity.TryGetContainer(out var container))
|
$"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode at {pos:coordinates} with intensity {totalIntensity} slope {slope}");
|
||||||
{
|
|
||||||
entity = container.Owner;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EntityManager.TryGetComponent(entity, out transform))
|
/// <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,
|
||||||
|
bool addLog = false)
|
||||||
{
|
{
|
||||||
|
if (totalIntensity <= 0 || slope <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_prototypeManager.TryIndex<ExplosionPrototype>(typeId, out var type))
|
||||||
|
{
|
||||||
|
Logger.Error($"Attempted to spawn unknown explosion prototype: {type}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var epicenter = transform.Coordinates;
|
if (addLog) // dont log if already created a separate, more detailed, log.
|
||||||
|
_logsSystem.Add(LogType.Explosion, LogImpact.High, $"Explosion spawned at {epicenter:coordinates} with intensity {totalIntensity} slope {slope}");
|
||||||
|
|
||||||
SpawnExplosion(epicenter, devastationRange, heavyImpactRange, lightImpactRange, flashRange, entity, user);
|
_explosionQueue.Enqueue(() => SpawnExplosion(epicenter, type, totalIntensity,
|
||||||
}
|
slope, maxTileIntensity));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SpawnExplosion(
|
/// <summary>
|
||||||
EntityCoordinates epicenter,
|
/// This function actually spawns the explosion. It returns an <see cref="Explosion"/> instance with
|
||||||
int devastationRange = 0,
|
/// information about the affected tiles for the explosion system to process. It will also trigger the
|
||||||
int heavyImpactRange = 0,
|
/// camera shake and sound effect.
|
||||||
int lightImpactRange = 0,
|
/// </summary>
|
||||||
int flashRange = 0,
|
private Explosion? SpawnExplosion(MapCoordinates epicenter,
|
||||||
EntityUid? entity = null,
|
ExplosionPrototype type,
|
||||||
EntityUid? user = null)
|
float totalIntensity,
|
||||||
|
float slope,
|
||||||
|
float maxTileIntensity)
|
||||||
{
|
{
|
||||||
var mapId = epicenter.GetMapId(EntityManager);
|
var results = GetExplosionTiles(epicenter, type.ID, totalIntensity, slope, maxTileIntensity);
|
||||||
if (mapId == MapId.Nullspace)
|
|
||||||
{
|
if (results == null)
|
||||||
return;
|
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,
|
||||||
|
EntityManager,
|
||||||
|
_mapManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
// logging
|
/// <summary>
|
||||||
var range = $"{devastationRange}/{heavyImpactRange}/{lightImpactRange}/{flashRange}";
|
/// Constructor for the shared <see cref="ExplosionEvent"/> using the server-exclusive explosion classes.
|
||||||
if (entity == null || !entity.Value.IsValid())
|
/// </summary>
|
||||||
|
internal ExplosionEvent GetExplosionEvent(MapCoordinates epicenter, string id, Matrix3 spaceMatrix, SpaceExplosion? spaceData, IEnumerable<GridExplosion> gridData, List<float> iterationIntensity)
|
||||||
{
|
{
|
||||||
_logSystem.Add(LogType.Explosion, LogImpact.High, $"Explosion spawned at {epicenter:coordinates} with range {range}");
|
var spaceTiles = spaceData?.TileLists;
|
||||||
}
|
|
||||||
else if (user == null || !user.Value.IsValid())
|
Dictionary<GridId, Dictionary<int, List<Vector2i>>> tileLists = new();
|
||||||
|
foreach (var grid in gridData)
|
||||||
{
|
{
|
||||||
_logSystem.Add(LogType.Explosion, LogImpact.High,
|
tileLists.Add(grid.Grid.Index, grid.TileLists);
|
||||||
$"{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);
|
return new ExplosionEvent(_explosionCounter, epicenter, id, iterationIntensity, spaceTiles, tileLists, spaceMatrix, spaceData?.TileSize ?? DefaultTileSize);
|
||||||
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);
|
private void CameraShake(float range, MapCoordinates epicenter, float totalIntensity)
|
||||||
FlashInRange(epicenter, flashRange);
|
{
|
||||||
|
var players = Filter.Empty();
|
||||||
|
players.AddInRange(epicenter, range, _playerManager, EntityManager);
|
||||||
|
|
||||||
|
foreach (var player in players.Recipients)
|
||||||
|
{
|
||||||
|
if (player.AttachedEntity is not EntityUid uid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var playerPos = Transform(player.AttachedEntity!.Value).WorldPosition;
|
||||||
|
var delta = epicenter.Position - playerPos;
|
||||||
|
|
||||||
|
if (delta.EqualsApprox(Vector2.Zero))
|
||||||
|
delta = new(0.01f, 0);
|
||||||
|
|
||||||
|
var distance = delta.Length;
|
||||||
|
var effect = 5 * MathF.Pow(totalIntensity, 0.5f) * (1 - distance / range);
|
||||||
|
if (effect > 0.01f)
|
||||||
|
_recoilSystem.KickCamera(uid, -delta.Normalized * effect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
304
Content.Server/Explosion/EntitySystems/GridExplosion.cs
Normal file
304
Content.Server/Explosion/EntitySystems/GridExplosion.cs
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using static Content.Server.Explosion.EntitySystems.ExplosionSystem;
|
||||||
|
|
||||||
|
namespace Content.Server.Explosion.EntitySystems;
|
||||||
|
|
||||||
|
public sealed class GridExplosion : TileExplosion
|
||||||
|
{
|
||||||
|
public IMapGrid Grid;
|
||||||
|
private bool _needToTransform = false;
|
||||||
|
|
||||||
|
private Matrix3 _matrix = Matrix3.Identity;
|
||||||
|
private Vector2 _offset;
|
||||||
|
|
||||||
|
// 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 int _typeIndex;
|
||||||
|
|
||||||
|
private UniqueVector2iSet _spaceTiles = new();
|
||||||
|
private UniqueVector2iSet _processedSpaceTiles = new();
|
||||||
|
|
||||||
|
public HashSet<Vector2i> SpaceJump = new();
|
||||||
|
|
||||||
|
private Dictionary<Vector2i, NeighborFlag> _edgeTiles;
|
||||||
|
|
||||||
|
public GridExplosion(
|
||||||
|
IMapGrid grid,
|
||||||
|
Dictionary<Vector2i, TileData> airtightMap,
|
||||||
|
float maxIntensity,
|
||||||
|
float intensityStepSize,
|
||||||
|
int typeIndex,
|
||||||
|
Dictionary<Vector2i, NeighborFlag> edgeTiles,
|
||||||
|
GridId? referenceGrid,
|
||||||
|
Matrix3 spaceMatrix,
|
||||||
|
Angle spaceAngle)
|
||||||
|
{
|
||||||
|
Grid = grid;
|
||||||
|
_airtightMap = airtightMap;
|
||||||
|
_maxIntensity = maxIntensity;
|
||||||
|
_intensityStepSize = intensityStepSize;
|
||||||
|
_typeIndex = typeIndex;
|
||||||
|
_edgeTiles = edgeTiles;
|
||||||
|
|
||||||
|
// initialise SpaceTiles
|
||||||
|
foreach (var (tile, spaceNeighbors) in _edgeTiles)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < NeighbourVectors.Length; i++)
|
||||||
|
{
|
||||||
|
var dir = (NeighborFlag) (1 << i);
|
||||||
|
if ((spaceNeighbors & dir) != NeighborFlag.Invalid)
|
||||||
|
_spaceTiles.Add(tile + NeighbourVectors[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 override void InitTile(Vector2i initialTile)
|
||||||
|
{
|
||||||
|
TileLists[0] = new() { initialTile };
|
||||||
|
|
||||||
|
if (_airtightMap.ContainsKey(initialTile))
|
||||||
|
EnteredBlockedTiles.Add(initialTile);
|
||||||
|
else
|
||||||
|
ProcessedTiles.Add(initialTile);
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
HashSet<Vector2i> toRemove = new();
|
||||||
|
foreach (var tile in freed)
|
||||||
|
{
|
||||||
|
if (!EnteredBlockedTiles.Add(tile))
|
||||||
|
toRemove.Add(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
freed.ExceptWith(toRemove);
|
||||||
|
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?
|
||||||
|
{
|
||||||
|
blocked = AnyNeighborBlocked(_edgeTiles[tile], blockedDirections); // 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?
|
||||||
|
var clearIteration = iteration + (int) MathF.Ceiling(tileData.ExplosionTolerance[_typeIndex] / _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;
|
||||||
|
sealIntegrity = tileData.ExplosionTolerance[_typeIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
160
Content.Server/Explosion/EntitySystems/SpaceExplosion.cs
Normal file
160
Content.Server/Explosion/EntitySystems/SpaceExplosion.cs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.Explosion.EntitySystems;
|
||||||
|
|
||||||
|
public 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, BlockedSpaceTile> _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>
|
||||||
|
public Dictionary<GridId, HashSet<Vector2i>> GridJump = new();
|
||||||
|
|
||||||
|
public ushort TileSize = ExplosionSystem.DefaultTileSize;
|
||||||
|
|
||||||
|
public SpaceExplosion(ExplosionSystem system, MapCoordinates epicentre, GridId? referenceGrid, List<GridId> localGrids, float maxDistance)
|
||||||
|
{
|
||||||
|
(_gridBlockMap, TileSize) = system.TransformGridEdges(epicentre, referenceGrid, localGrids, maxDistance);
|
||||||
|
system.GetUnblockedDirections(_gridBlockMap, TileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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(BlockedSpaceTile 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void InitTile(Vector2i initialTile)
|
||||||
|
{
|
||||||
|
ProcessedTiles.Add(initialTile);
|
||||||
|
TileLists[0] = new() { 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
191
Content.Server/Explosion/EntitySystems/TileExplosion.cs
Normal file
191
Content.Server/Explosion/EntitySystems/TileExplosion.cs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
using Content.Shared.Atmos;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
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>
|
||||||
|
public abstract class TileExplosion
|
||||||
|
{
|
||||||
|
// Main tile data sets, mapping iterations onto tile lists
|
||||||
|
public 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 UniqueVector2iSet ProcessedTiles = new();
|
||||||
|
protected UniqueVector2iSet UnenteredBlockedTiles = new();
|
||||||
|
protected UniqueVector2iSet EnteredBlockedTiles = new();
|
||||||
|
|
||||||
|
public abstract void InitTile(Vector2i 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>
|
||||||
|
public void CleanUp()
|
||||||
|
{
|
||||||
|
foreach (var (iteration, blocked) in BlockedTileLists)
|
||||||
|
{
|
||||||
|
if (TileLists.TryGetValue(iteration, out var tiles))
|
||||||
|
tiles.AddRange(blocked);
|
||||||
|
else
|
||||||
|
TileLists[iteration] = blocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a data structure can be used to ensure the uniqueness of Vector2i indices.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This basically exists to replace the use of HashSet<Vector2i> if all you need is the the functions Contains()
|
||||||
|
/// and Add(). This is both faster and apparently allocates less. Does not support iterating over contents
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class UniqueVector2iSet
|
||||||
|
{
|
||||||
|
private const int ChunkSize = 32; // # of bits in an integer.
|
||||||
|
|
||||||
|
private Dictionary<Vector2i, VectorChunk> _chunks = new();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public Vector2i ToChunkIndices(Vector2i indices)
|
||||||
|
{
|
||||||
|
var x = (int) Math.Floor(indices.X / (float) ChunkSize);
|
||||||
|
var y = (int) Math.Floor(indices.Y / (float) ChunkSize);
|
||||||
|
return new Vector2i(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool Add(Vector2i index)
|
||||||
|
{
|
||||||
|
var chunkIndex = ToChunkIndices(index);
|
||||||
|
if (_chunks.TryGetValue(chunkIndex, out var chunk))
|
||||||
|
{
|
||||||
|
return chunk.Add(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk = new();
|
||||||
|
chunk.Add(index);
|
||||||
|
_chunks[chunkIndex] = chunk;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool Contains(Vector2i index)
|
||||||
|
{
|
||||||
|
if (!_chunks.TryGetValue(ToChunkIndices(index), out var chunk))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return chunk.Contains(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class VectorChunk
|
||||||
|
{
|
||||||
|
// 32*32 chunk represented via 32 ints with 32 bits each. Basic testing showed that this was faster than using
|
||||||
|
// 16-sized chunks with ushorts, a bool[,], or just having each chunk be a HashSet.
|
||||||
|
private readonly int[] _tiles = new int[ChunkSize];
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool Add(Vector2i index)
|
||||||
|
{
|
||||||
|
var x = MathHelper.Mod(index.X, ChunkSize);
|
||||||
|
var y = MathHelper.Mod(index.Y, ChunkSize);
|
||||||
|
|
||||||
|
var oldFlags = _tiles[x];
|
||||||
|
var newFlags = oldFlags | (1 << y);
|
||||||
|
|
||||||
|
if (newFlags == oldFlags)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_tiles[x] = newFlags;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool Contains(Vector2i index)
|
||||||
|
{
|
||||||
|
var x = MathHelper.Mod(index.X, ChunkSize);
|
||||||
|
var y = MathHelper.Mod(index.Y, ChunkSize);
|
||||||
|
return (_tiles[x] & (1 << y)) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,33 +55,11 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
SubscribeLocalEvent<ToggleDoorOnTriggerComponent, TriggerEvent>(HandleDoorTrigger);
|
SubscribeLocalEvent<ToggleDoorOnTriggerComponent, TriggerEvent>(HandleDoorTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Explosions
|
|
||||||
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
|
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
|
||||||
{
|
{
|
||||||
if (!EntityManager.TryGetComponent(uid, out ExplosiveComponent? explosiveComponent)) return;
|
_explosions.TriggerExplosive(uid, user: args.User);
|
||||||
|
|
||||||
Explode(uid, explosiveComponent, args.User);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
#region Flash
|
||||||
private void HandleFlashTrigger(EntityUid uid, FlashOnTriggerComponent component, TriggerEvent args)
|
private void HandleFlashTrigger(EntityUid uid, FlashOnTriggerComponent component, TriggerEvent args)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ namespace Content.Server.NodeContainer.EntitySystems
|
|||||||
private int _gen = 1;
|
private int _gen = 1;
|
||||||
private int _groupNetIdCounter = 1;
|
private int _groupNetIdCounter = 1;
|
||||||
|
|
||||||
|
public bool Snoozing = false;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
@@ -138,9 +140,12 @@ namespace Content.Server.NodeContainer.EntitySystems
|
|||||||
{
|
{
|
||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
|
|
||||||
|
if (!Snoozing)
|
||||||
|
{
|
||||||
DoGroupUpdates();
|
DoGroupUpdates();
|
||||||
VisDoUpdate(frameTime);
|
VisDoUpdate(frameTime);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DoGroupUpdates()
|
private void DoGroupUpdates()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using Content.Shared.Containers.ItemSlots;
|
using Content.Shared.Containers.ItemSlots;
|
||||||
|
using Content.Shared.Explosion;
|
||||||
using Content.Shared.Nuke;
|
using Content.Shared.Nuke;
|
||||||
using Content.Shared.Sound;
|
using Content.Shared.Sound;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
|
||||||
namespace Content.Server.Nuke
|
namespace Content.Server.Nuke
|
||||||
{
|
{
|
||||||
@@ -36,13 +38,6 @@ namespace Content.Server.Nuke
|
|||||||
[DataField("diskSlot")]
|
[DataField("diskSlot")]
|
||||||
public ItemSlot DiskSlot = new();
|
public ItemSlot DiskSlot = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Annihilation radius in which all human players will be gibed
|
|
||||||
/// </summary>
|
|
||||||
[DataField("blastRadius")]
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
public int BlastRadius = 200;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// After this time nuke will play last alert sound
|
/// After this time nuke will play last alert sound
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -67,6 +62,48 @@ namespace Content.Server.Nuke
|
|||||||
[DataField("disarmSound")]
|
[DataField("disarmSound")]
|
||||||
public SoundSpecifier DisarmSound = new SoundPathSpecifier("/Audio/Misc/notice2.ogg");
|
public SoundSpecifier DisarmSound = new SoundPathSpecifier("/Audio/Misc/notice2.ogg");
|
||||||
|
|
||||||
|
// These datafields here are duplicates of those in explosive component. But I'm hesitant to use explosive
|
||||||
|
// component, just in case at some point, somehow, when grenade crafting added in someone manages to wire up a
|
||||||
|
// proximity trigger or something to the nuke and set it off prematurely. I want to make sure they MEAN to set of
|
||||||
|
// the nuke.
|
||||||
|
#region ExplosiveComponent
|
||||||
|
/// <summary>
|
||||||
|
/// The explosion prototype. This determines the damage types, the tile-break chance, and some visual
|
||||||
|
/// information (e.g., the light that the explosion gives off).
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("explosionType", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))]
|
||||||
|
public string ExplosionType = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum intensity the explosion can have on a single time. This limits the maximum damage and tile
|
||||||
|
/// break chance the explosion can achieve at any given location.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("maxIntensity")]
|
||||||
|
public float MaxIntensity = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How quickly the intensity drops off as you move away from the epicenter.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("intensitySlope")]
|
||||||
|
public float IntensitySlope = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total intensity of this explosion. The radius of the explosion scales like the cube root of this
|
||||||
|
/// number (see <see cref="ExplosionSystem.RadiusToIntensity"/>).
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("totalIntensity")]
|
||||||
|
public float TotalIntensity = 100000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Avoid somehow double-triggering this explosion.
|
||||||
|
/// </summary>
|
||||||
|
public bool Exploded;
|
||||||
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Time until explosion in seconds.
|
/// Time until explosion in seconds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
using Content.Server.Construction.Components;
|
using Content.Server.Construction.Components;
|
||||||
using Content.Server.Coordinates.Helpers;
|
using Content.Server.Coordinates.Helpers;
|
||||||
|
using Content.Server.Explosion.EntitySystems;
|
||||||
using Content.Server.Popups;
|
using Content.Server.Popups;
|
||||||
using Content.Server.UserInterface;
|
using Content.Server.UserInterface;
|
||||||
using Content.Shared.Audio;
|
using Content.Shared.Audio;
|
||||||
using Content.Shared.Body.Components;
|
|
||||||
using Content.Shared.Containers.ItemSlots;
|
using Content.Shared.Containers.ItemSlots;
|
||||||
using Content.Shared.Nuke;
|
using Content.Shared.Nuke;
|
||||||
using Content.Shared.Sound;
|
using Content.Shared.Sound;
|
||||||
@@ -19,7 +19,7 @@ namespace Content.Server.Nuke
|
|||||||
[Dependency] private readonly NukeCodeSystem _codes = default!;
|
[Dependency] private readonly NukeCodeSystem _codes = default!;
|
||||||
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
|
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
|
||||||
[Dependency] private readonly PopupSystem _popups = default!;
|
[Dependency] private readonly PopupSystem _popups = default!;
|
||||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
[Dependency] private readonly ExplosionSystem _explosions = default!;
|
||||||
[Dependency] private readonly IChatManager _chat = default!;
|
[Dependency] private readonly IChatManager _chat = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
@@ -388,19 +388,16 @@ namespace Content.Server.Nuke
|
|||||||
if (!Resolve(uid, ref component, ref transform))
|
if (!Resolve(uid, ref component, ref transform))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// gib anyone in a blast radius
|
if (component.Exploded)
|
||||||
// its lame, but will work for now
|
return;
|
||||||
var pos = transform.Coordinates;
|
|
||||||
var ents = _lookup.GetEntitiesInRange(pos, component.BlastRadius);
|
|
||||||
foreach (var ent in ents)
|
|
||||||
{
|
|
||||||
var entUid = ent;
|
|
||||||
if (!EntityManager.EntityExists(entUid))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (EntityManager.TryGetComponent(entUid, out SharedBodyComponent? body))
|
component.Exploded = true;
|
||||||
body.Gib();
|
|
||||||
}
|
_explosions.QueueExplosion(uid,
|
||||||
|
component.ExplosionType,
|
||||||
|
component.TotalIntensity,
|
||||||
|
component.IntensitySlope,
|
||||||
|
component.MaxIntensity);
|
||||||
|
|
||||||
EntityManager.DeleteEntity(uid);
|
EntityManager.DeleteEntity(uid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Content.Server.Pointing.EntitySystems;
|
using Content.Server.Pointing.EntitySystems;
|
||||||
using Content.Shared.Pointing.Components;
|
using Content.Shared.Pointing.Components;
|
||||||
using Content.Shared.Sound;
|
|
||||||
using Robust.Shared.Analyzers;
|
using Robust.Shared.Analyzers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
@@ -26,8 +25,5 @@ namespace Content.Server.Pointing.Components
|
|||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
[DataField("chasingTime")]
|
[DataField("chasingTime")]
|
||||||
public float ChasingTime = 1;
|
public float ChasingTime = 1;
|
||||||
|
|
||||||
[DataField("explosionSound")]
|
|
||||||
public SoundSpecifier ExplosionSound = new SoundCollectionSpecifier("explosion");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using Content.Shared.MobState.Components;
|
|||||||
using Content.Shared.Pointing.Components;
|
using Content.Shared.Pointing.Components;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
@@ -19,8 +18,7 @@ namespace Content.Server.Pointing.EntitySystems
|
|||||||
internal sealed class RoguePointingSystem : EntitySystem
|
internal sealed class RoguePointingSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly ExplosionSystem _explosion = default!;
|
||||||
[Dependency] private readonly ExplosionSystem _explosions = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -103,10 +101,7 @@ namespace Content.Server.Pointing.EntitySystems
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_explosion.QueueExplosion(uid, ExplosionSystem.DefaultExplosionPrototypeId, 50, 3, 10);
|
||||||
_explosions.SpawnExplosion(uid, 0, 2, 1, 1);
|
|
||||||
SoundSystem.Play(Filter.Pvs(uid, entityManager: EntityManager), component.ExplosionSound.GetSound(), uid);
|
|
||||||
|
|
||||||
EntityManager.QueueDeleteEntity(uid);
|
EntityManager.QueueDeleteEntity(uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,10 +88,10 @@ public sealed class PowerCellSystem : SharedPowerCellSystem
|
|||||||
if (!Resolve(uid, ref battery))
|
if (!Resolve(uid, ref battery))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var heavy = (int) Math.Ceiling(Math.Sqrt(battery.CurrentCharge) / 60);
|
var radius = MathF.Min(5, MathF.Ceiling(MathF.Sqrt(battery.CurrentCharge) / 30));
|
||||||
var light = (int) Math.Ceiling(Math.Sqrt(battery.CurrentCharge) / 30);
|
battery.CurrentCharge = 0;
|
||||||
|
|
||||||
_explosionSystem.SpawnExplosion(uid, 0, heavy, light, light * 2);
|
_explosionSystem.TriggerExplosive(uid, radius: radius);
|
||||||
QueueDel(uid);
|
QueueDel(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace Content.Server.Storage.Components
|
|||||||
[Virtual]
|
[Virtual]
|
||||||
[ComponentReference(typeof(IActivate))]
|
[ComponentReference(typeof(IActivate))]
|
||||||
[ComponentReference(typeof(IStorageComponent))]
|
[ComponentReference(typeof(IStorageComponent))]
|
||||||
public class EntityStorageComponent : Component, IActivate, IStorageComponent, IInteractUsing, IDestroyAct, IExAct
|
public class EntityStorageComponent : Component, IActivate, IStorageComponent, IInteractUsing, IDestroyAct
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||||
|
|
||||||
@@ -478,24 +478,6 @@ namespace Content.Server.Storage.Components
|
|||||||
var entityLookup = EntitySystem.Get<EntityLookupSystem>();
|
var entityLookup = EntitySystem.Get<EntityLookupSystem>();
|
||||||
return entityLookup.GetEntitiesIntersecting(Owner, _enteringRange, LookupFlags.Approximate);
|
return entityLookup.GetEntitiesIntersecting(Owner, _enteringRange, LookupFlags.Approximate);
|
||||||
}
|
}
|
||||||
|
|
||||||
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (eventArgs.Severity < ExplosionSeverity.Heavy)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var containedEntities = Contents.ContainedEntities.ToList();
|
|
||||||
foreach (var entity in containedEntities)
|
|
||||||
{
|
|
||||||
var exActs = _entMan.GetComponents<IExAct>(entity).ToArray();
|
|
||||||
foreach (var exAct in exActs)
|
|
||||||
{
|
|
||||||
exAct.OnExplosion(eventArgs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class StorageOpenAttemptEvent : CancellableEntityEventArgs
|
public sealed class StorageOpenAttemptEvent : CancellableEntityEventArgs
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace Content.Server.Storage.Components
|
|||||||
[ComponentReference(typeof(IActivate))]
|
[ComponentReference(typeof(IActivate))]
|
||||||
[ComponentReference(typeof(IStorageComponent))]
|
[ComponentReference(typeof(IStorageComponent))]
|
||||||
[ComponentReference(typeof(SharedStorageComponent))]
|
[ComponentReference(typeof(SharedStorageComponent))]
|
||||||
public sealed class ServerStorageComponent : SharedStorageComponent, IInteractUsing, IActivate, IStorageComponent, IDestroyAct, IExAct, IAfterInteract
|
public sealed class ServerStorageComponent : SharedStorageComponent, IInteractUsing, IActivate, IStorageComponent, IDestroyAct, IAfterInteract
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
||||||
@@ -630,30 +630,6 @@ namespace Content.Server.Storage.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (eventArgs.Severity < ExplosionSeverity.Heavy)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var storedEntities = StoredEntities?.ToList();
|
|
||||||
|
|
||||||
if (storedEntities == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entity in storedEntities)
|
|
||||||
{
|
|
||||||
var exActs = _entityManager.GetComponents<IExAct>(entity).ToArray();
|
|
||||||
foreach (var exAct in exActs)
|
|
||||||
{
|
|
||||||
exAct.OnExplosion(eventArgs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PlaySoundCollection()
|
private void PlaySoundCollection()
|
||||||
{
|
{
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), StorageSoundCollection.GetSound(), Owner, AudioParams.Default);
|
SoundSystem.Play(Filter.Pvs(Owner), StorageSoundCollection.GetSound(), Owner, AudioParams.Default);
|
||||||
|
|||||||
@@ -29,21 +29,6 @@ namespace Content.Shared.Acts
|
|||||||
void OnBreak(BreakageEventArgs eventArgs);
|
void OnBreak(BreakageEventArgs eventArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IExAct
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Called when explosion reaches the entity
|
|
||||||
/// </summary>
|
|
||||||
void OnExplosion(ExplosionEventArgs eventArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ExplosionEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
public EntityCoordinates Source { get; set; }
|
|
||||||
public EntityUid Target { get; set; }
|
|
||||||
public ExplosionSeverity Severity { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class ActSystem : EntitySystem
|
public sealed class ActSystem : EntitySystem
|
||||||
{
|
{
|
||||||
@@ -59,23 +44,7 @@ namespace Content.Shared.Acts
|
|||||||
destroyAct.OnDestroy(eventArgs);
|
destroyAct.OnDestroy(eventArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
EntityManager.QueueDeleteEntity(owner);
|
QueueDel(owner);
|
||||||
}
|
|
||||||
|
|
||||||
public void HandleExplosion(EntityCoordinates source, EntityUid target, ExplosionSeverity severity)
|
|
||||||
{
|
|
||||||
var eventArgs = new ExplosionEventArgs
|
|
||||||
{
|
|
||||||
Source = source,
|
|
||||||
Target = target,
|
|
||||||
Severity = severity
|
|
||||||
};
|
|
||||||
var exActs = EntityManager.GetComponents<IExAct>(target).ToList();
|
|
||||||
|
|
||||||
foreach (var exAct in exActs)
|
|
||||||
{
|
|
||||||
exAct.OnExplosion(eventArgs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleBreakage(EntityUid owner)
|
public void HandleBreakage(EntityUid owner)
|
||||||
@@ -89,11 +58,4 @@ namespace Content.Shared.Acts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ExplosionSeverity
|
|
||||||
{
|
|
||||||
Light,
|
|
||||||
Heavy,
|
|
||||||
Destruction,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
Content.Shared/Administration/SpawnExplosionEuiMsg.cs
Normal file
52
Content.Shared/Administration/SpawnExplosionEuiMsg.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using Content.Shared.Eui;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Content.Shared.Explosion;
|
||||||
|
|
||||||
|
namespace Content.Shared.Administration;
|
||||||
|
|
||||||
|
public static class SpawnExplosionEuiMsg
|
||||||
|
{
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class Close : EuiMessageBase { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This message is sent to the server to request explosion preview data.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PreviewRequest : EuiMessageBase
|
||||||
|
{
|
||||||
|
public readonly MapCoordinates Epicenter;
|
||||||
|
public readonly string TypeId;
|
||||||
|
public readonly float TotalIntensity;
|
||||||
|
public readonly float IntensitySlope;
|
||||||
|
public readonly float MaxIntensity;
|
||||||
|
|
||||||
|
public PreviewRequest(MapCoordinates epicenter, string typeId, float totalIntensity, float intensitySlope, float maxIntensity)
|
||||||
|
{
|
||||||
|
Epicenter = epicenter;
|
||||||
|
TypeId = typeId;
|
||||||
|
TotalIntensity = totalIntensity;
|
||||||
|
IntensitySlope = intensitySlope;
|
||||||
|
MaxIntensity = maxIntensity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This message is used to send explosion-preview data to the client.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PreviewData : EuiMessageBase
|
||||||
|
{
|
||||||
|
public readonly float Slope;
|
||||||
|
public readonly float TotalIntensity;
|
||||||
|
public readonly ExplosionEvent Explosion;
|
||||||
|
|
||||||
|
public PreviewData(ExplosionEvent explosion, float slope, float totalIntensity)
|
||||||
|
{
|
||||||
|
Slope = slope;
|
||||||
|
TotalIntensity = totalIntensity;
|
||||||
|
Explosion = explosion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -408,6 +408,106 @@ namespace Content.Shared.CCVar
|
|||||||
public static readonly CVarDef<bool> AdminAnnounceLogout =
|
public static readonly CVarDef<bool> AdminAnnounceLogout =
|
||||||
CVarDef.Create("admin.announce_logout", true, CVar.SERVERONLY);
|
CVarDef.Create("admin.announce_logout", true, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Explosions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many tiles the explosion system will process per tick
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Setting this too high will put a large load on a single tick. Setting this too low will lead to
|
||||||
|
/// unnaturally "slow" explosions.
|
||||||
|
/// </remarks>
|
||||||
|
public static readonly CVarDef<int> ExplosionTilesPerTick =
|
||||||
|
CVarDef.Create("explosion.tiles_per_tick", 100, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upper limit on the size of an explosion before physics-throwing is disabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Large nukes tend to generate a lot of shrapnel that flies through space. This can functionally cripple
|
||||||
|
/// the server TPS for a while after an explosion (or even during, if the explosion is processed
|
||||||
|
/// incrementally.
|
||||||
|
/// </remarks>
|
||||||
|
public static readonly CVarDef<int> ExplosionThrowLimit =
|
||||||
|
CVarDef.Create("explosion.throw_limit", 400, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If this is true, explosion processing will pause the NodeGroupSystem to pause updating.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This only takes effect if an explosion needs more than one tick to process (i.e., covers more than <see
|
||||||
|
/// cref="ExplosionTilesPerTick"/> tiles). If this is not enabled, the node-system will rebuild its graph
|
||||||
|
/// every tick as the explosion shreds the station, causing significant slowdown.
|
||||||
|
/// </remarks>
|
||||||
|
public static readonly CVarDef<bool> ExplosionSleepNodeSys =
|
||||||
|
CVarDef.Create("explosion.node_sleep", true, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upper limit on the total area that an explosion can affect before the neighbor-finding algorithm just
|
||||||
|
/// stops. Defaults to a 60-rile radius explosion.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Actual area may be larger, as it currently doesn't terminate mid neighbor finding. I.e., area may be that of a ~51 tile radius circle instead.
|
||||||
|
/// </remarks>
|
||||||
|
public static readonly CVarDef<int> ExplosionMaxArea =
|
||||||
|
CVarDef.Create("explosion.max_area", (int) 3.14f * 50 * 50, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upper limit on the number of neighbor finding steps for the explosion system neighbor-finding algorithm.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Effectively places an upper limit on the range that any explosion can have. In the vast majority of
|
||||||
|
/// instances, <see cref="ExplosionMaxArea"/> will likely be hit before this becomes a limiting factor.
|
||||||
|
/// </remarks>
|
||||||
|
public static readonly CVarDef<int> ExplosionMaxIterations =
|
||||||
|
CVarDef.Create("explosion.max_iterations", 150, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Max Time in milliseconds to spend processing explosions every tick.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This time limiting is not perfectly implemented. Firstly, a significant chunk of processing time happens
|
||||||
|
/// due to queued entity deletions, which happen outside of the system update code. Secondly, explosion
|
||||||
|
/// spawning cannot currently be interrupted & resumed, and may lead to exceeding this time limit.
|
||||||
|
/// </remarks>
|
||||||
|
public static readonly CVarDef<float> ExplosionMaxProcessingTime =
|
||||||
|
CVarDef.Create("explosion.max_tick_time", 7f, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If the explosion is being processed incrementally over several ticks, this variable determines whether
|
||||||
|
/// updating the grid tiles should be done incrementally at the end of every tick, or only once the explosion has finished processing.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The most notable consequence of this change is that explosions will only punch a hole in the station &
|
||||||
|
/// create a vacumm once they have finished exploding. So airlocks will no longer slam shut as the explosion
|
||||||
|
/// expands, just suddenly at the end.
|
||||||
|
/// </remarks>
|
||||||
|
public static readonly CVarDef<bool> ExplosionIncrementalTileBreaking =
|
||||||
|
CVarDef.Create("explosion.incremental_tile", false, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client-side explosion visuals: for how many seconds should an explosion stay on-screen once it has
|
||||||
|
/// finished expanding?
|
||||||
|
/// </summary>
|
||||||
|
public static readonly CVarDef<float> ExplosionPersistence =
|
||||||
|
CVarDef.Create("explosion.persistence", 0.3f, CVar.REPLICATED);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If an explosion covers a larger area than this number, the damaging/processing will always start during
|
||||||
|
/// the next tick, instead of during the same tick that the explosion was generated in.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This value can be used to ensure that for large explosions the area/tile calculation and the explosion
|
||||||
|
/// processing/damaging occurs in separate ticks. This helps reduce the single-tick lag if both <see
|
||||||
|
/// cref="ExplosionMaxProcessingTime"/> and <see cref="ExplosionTilesPerTick"/> are large. I.e., instead of
|
||||||
|
/// a single tick explosion, this cvar allows for a configuration that results in a two-tick explosion,
|
||||||
|
/// though most of the computational cost is still in the second tick.
|
||||||
|
/// </remarks>
|
||||||
|
public static readonly CVarDef<int> ExplosionSingleTickAreaLimit =
|
||||||
|
CVarDef.Create("explosion.single_tick_area_limit", 400, CVar.SERVERONLY);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Admin logs
|
* Admin logs
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace Content.Shared.Damage
|
|||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
[NetworkedComponent()]
|
[NetworkedComponent()]
|
||||||
[Friend(typeof(DamageableSystem))]
|
[Friend(typeof(DamageableSystem))]
|
||||||
public sealed class DamageableComponent : Component, IRadiationAct, IExAct
|
public sealed class DamageableComponent : Component, IRadiationAct
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This <see cref="DamageContainerPrototype"/> specifies what damage types are supported by this component.
|
/// This <see cref="DamageContainerPrototype"/> specifies what damage types are supported by this component.
|
||||||
@@ -92,27 +92,6 @@ namespace Content.Shared.Damage
|
|||||||
|
|
||||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner, damage);
|
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner, damage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO EXPLOSION Remove this.
|
|
||||||
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
var damageValue = eventArgs.Severity switch
|
|
||||||
{
|
|
||||||
ExplosionSeverity.Light => FixedPoint2.New(20),
|
|
||||||
ExplosionSeverity.Heavy => FixedPoint2.New(60),
|
|
||||||
ExplosionSeverity.Destruction => FixedPoint2.New(250),
|
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Explosion should really just be a damage group instead of a list of types.
|
|
||||||
DamageSpecifier damage = new();
|
|
||||||
foreach (var typeID in ExplosionDamageTypeIDs)
|
|
||||||
{
|
|
||||||
damage.DamageDict.Add(typeID, damageValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner, damage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
|
|||||||
@@ -107,9 +107,9 @@ namespace Content.Shared.Damage
|
|||||||
/// null if the user had no applicable components that can take damage.
|
/// null if the user had no applicable components that can take damage.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public DamageSpecifier? TryChangeDamage(EntityUid? uid, DamageSpecifier damage, bool ignoreResistances = false,
|
public DamageSpecifier? TryChangeDamage(EntityUid? uid, DamageSpecifier damage, bool ignoreResistances = false,
|
||||||
bool interruptsDoAfters = true)
|
bool interruptsDoAfters = true, DamageableComponent? damageable = null)
|
||||||
{
|
{
|
||||||
if (!EntityManager.TryGetComponent<DamageableComponent>(uid, out var damageable))
|
if (!uid.HasValue || !Resolve(uid.Value, ref damageable, false))
|
||||||
{
|
{
|
||||||
// TODO BODY SYSTEM pass damage onto body system
|
// TODO BODY SYSTEM pass damage onto body system
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
85
Content.Shared/Explosion/ExplosionEvents.cs
Normal file
85
Content.Shared/Explosion/ExplosionEvents.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
using Content.Shared.Inventory;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Explosion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised directed at an entity to determine its explosion resistance, probably right before it is about to be
|
||||||
|
/// damaged by one.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetExplosionResistanceEvent : EntityEventArgs, IInventoryRelayEvent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Can be set to whatever, but currently is being additively increased by components & clothing. So think twice
|
||||||
|
/// before multiplying or directly setting this.
|
||||||
|
/// </summary>
|
||||||
|
public float Resistance = 0;
|
||||||
|
|
||||||
|
public readonly string ExplotionPrototype;
|
||||||
|
|
||||||
|
SlotFlags IInventoryRelayEvent.TargetSlots => ~SlotFlags.POCKET;
|
||||||
|
|
||||||
|
public GetExplosionResistanceEvent(string id)
|
||||||
|
{
|
||||||
|
ExplotionPrototype = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An explosion event. Used for client side rendering.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class ExplosionEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public MapCoordinates Epicenter;
|
||||||
|
|
||||||
|
public Dictionary<int, List<Vector2i>>? SpaceTiles;
|
||||||
|
public Dictionary<GridId, Dictionary<int, List<Vector2i>>> Tiles;
|
||||||
|
|
||||||
|
public List<float> Intensity;
|
||||||
|
|
||||||
|
public string TypeID;
|
||||||
|
|
||||||
|
public Matrix3 SpaceMatrix;
|
||||||
|
|
||||||
|
public int ExplosionId;
|
||||||
|
|
||||||
|
public ushort SpaceTileSize;
|
||||||
|
|
||||||
|
public ExplosionEvent(
|
||||||
|
int explosionId,
|
||||||
|
MapCoordinates epicenter,
|
||||||
|
string typeID,
|
||||||
|
List<float> intensity,
|
||||||
|
Dictionary<int, List<Vector2i>>? spaceTiles,
|
||||||
|
Dictionary<GridId, Dictionary<int, List<Vector2i>>> tiles,
|
||||||
|
Matrix3 spaceMatrix,
|
||||||
|
ushort spaceTileSize)
|
||||||
|
{
|
||||||
|
Epicenter = epicenter;
|
||||||
|
SpaceTiles = spaceTiles;
|
||||||
|
Tiles = tiles;
|
||||||
|
Intensity = intensity;
|
||||||
|
TypeID = typeID;
|
||||||
|
SpaceMatrix = spaceMatrix;
|
||||||
|
ExplosionId = explosionId;
|
||||||
|
SpaceTileSize = spaceTileSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update visual rendering of the explosion to correspond to the servers processing of it.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class ExplosionOverlayUpdateEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public int Index;
|
||||||
|
public int ExplosionId;
|
||||||
|
|
||||||
|
public ExplosionOverlayUpdateEvent(int explosionId, int index)
|
||||||
|
{
|
||||||
|
Index = index;
|
||||||
|
ExplosionId = explosionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
104
Content.Shared/Explosion/ExplosionPrototype.cs
Normal file
104
Content.Shared/Explosion/ExplosionPrototype.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using Content.Shared.Damage;
|
||||||
|
using Content.Shared.Sound;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Shared.Explosion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Explosion Prototype. Determines damage, tile break probabilities, and visuals.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Does not currently support prototype hot-reloading. The explosion-intensity required to destroy airtight
|
||||||
|
/// entities is evaluated and stored by the explosion system. Adding or removing a prototype would require updating
|
||||||
|
/// that map of airtight entities. This could be done, but is just not yet implemented.
|
||||||
|
/// </remarks>
|
||||||
|
[Prototype("explosion")]
|
||||||
|
public sealed class ExplosionPrototype : IPrototype
|
||||||
|
{
|
||||||
|
[DataField("id", required: true)]
|
||||||
|
public string ID { get; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Damage to deal to entities. This is scaled by the explosion intensity.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("damagePerIntensity", required: true)]
|
||||||
|
public readonly DamageSpecifier DamagePerIntensity = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This set of points, together with <see cref="_tileBreakIntensity"/> define a function that maps the
|
||||||
|
/// explosion intensity to a tile break chance via linear interpolation.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("tileBreakChance")]
|
||||||
|
private readonly float[] _tileBreakChance = { 0f, 1f };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This set of points, together with <see cref="_tileBreakChance"/> define a function that maps the
|
||||||
|
/// explosion intensity to a tile break chance via linear interpolation.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("tileBreakIntensity")]
|
||||||
|
private readonly float[] _tileBreakIntensity = {0f, 15f };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When a tile is broken by an explosion, the intensity is reduced by this amount and is used to try and
|
||||||
|
/// break the tile a second time. This is repeated until a roll fails or the tile has become space.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If this number is too small, even relatively weak explosions can have a non-zero
|
||||||
|
/// chance to create a space tile.
|
||||||
|
/// </remarks>
|
||||||
|
[DataField("tileBreakRerollReduction")]
|
||||||
|
public readonly float TileBreakRerollReduction = 10f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Color emitted by a point light at the center of the explosion.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("lightColor")]
|
||||||
|
public readonly Color LightColor = Color.Orange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Color used to modulate the fire texture.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("fireColor")]
|
||||||
|
public readonly Color? FireColor;
|
||||||
|
|
||||||
|
[DataField("Sound")]
|
||||||
|
public readonly SoundSpecifier Sound = new SoundCollectionSpecifier("explosion");
|
||||||
|
|
||||||
|
[DataField("texturePath")]
|
||||||
|
public readonly ResourcePath TexturePath = new("/Textures/Effects/fire.rsi");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How intense does the explosion have to be at a tile to advance to the next fire texture state?
|
||||||
|
/// </summary>
|
||||||
|
[DataField("intensityPerState")]
|
||||||
|
public float IntensityPerState = 12;
|
||||||
|
|
||||||
|
// Theres probably a better way to do this. Currently Atmos just hard codes a constant int, so I have no one to
|
||||||
|
// steal code from.
|
||||||
|
[DataField("fireStates")]
|
||||||
|
public readonly int FireStates = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Basic function for linear interpolation of the _tileBreakChance and _tileBreakIntensity arrays
|
||||||
|
/// </summary>
|
||||||
|
public float TileBreakChance(float intensity)
|
||||||
|
{
|
||||||
|
if (_tileBreakChance.Length == 0 || _tileBreakChance.Length != _tileBreakIntensity.Length)
|
||||||
|
{
|
||||||
|
Logger.Error($"Malformed tile break chance definitions for explosion prototype: {ID}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intensity >= _tileBreakIntensity[^1] || _tileBreakIntensity.Length == 1)
|
||||||
|
return _tileBreakChance[^1];
|
||||||
|
|
||||||
|
if (intensity <= _tileBreakIntensity[0])
|
||||||
|
return _tileBreakChance[0];
|
||||||
|
|
||||||
|
int i = Array.FindIndex(_tileBreakIntensity, k => k >= intensity);
|
||||||
|
|
||||||
|
var slope = (_tileBreakChance[i] - _tileBreakChance[i - 1]) / (_tileBreakIntensity[i] - _tileBreakIntensity[i - 1]);
|
||||||
|
return _tileBreakChance[i - 1] + slope * (intensity - _tileBreakIntensity[i - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,10 @@
|
|||||||
using System.Linq;
|
|
||||||
using Content.Shared.Acts;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
|
||||||
namespace Content.Shared.Inventory;
|
namespace Content.Shared.Inventory;
|
||||||
|
|
||||||
public abstract class InventoryComponent : Component, IExAct
|
public abstract class InventoryComponent : Component
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
|
||||||
|
|
||||||
[DataField("templateId", required: true,
|
[DataField("templateId", required: true,
|
||||||
customTypeSerializer: typeof(PrototypeIdSerializer<InventoryTemplatePrototype>))]
|
customTypeSerializer: typeof(PrototypeIdSerializer<InventoryTemplatePrototype>))]
|
||||||
public string TemplateId { get; } = "human";
|
public string TemplateId { get; } = "human";
|
||||||
|
|
||||||
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (eventArgs.Severity < ExplosionSeverity.Heavy)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (EntitySystem.Get<InventorySystem>()
|
|
||||||
.TryGetContainerSlotEnumerator(Owner, out var enumerator, this))
|
|
||||||
{
|
|
||||||
while (enumerator.MoveNext(out var container))
|
|
||||||
{
|
|
||||||
if (!container.ContainedEntity.HasValue) continue;
|
|
||||||
foreach (var exAct in _entityManager.GetComponents<IExAct>(container.ContainedEntity.Value).ToArray())
|
|
||||||
{
|
|
||||||
exAct.OnExplosion(eventArgs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using Content.Shared.Damage;
|
using Content.Shared.Damage;
|
||||||
using Content.Shared.Electrocution;
|
using Content.Shared.Electrocution;
|
||||||
|
using Content.Shared.Explosion;
|
||||||
using Content.Shared.Movement.EntitySystems;
|
using Content.Shared.Movement.EntitySystems;
|
||||||
using Content.Shared.Slippery;
|
using Content.Shared.Slippery;
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
|
|
||||||
namespace Content.Shared.Inventory;
|
namespace Content.Shared.Inventory;
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ public partial class InventorySystem
|
|||||||
SubscribeLocalEvent<InventoryComponent, ElectrocutionAttemptEvent>(RelayInventoryEvent);
|
SubscribeLocalEvent<InventoryComponent, ElectrocutionAttemptEvent>(RelayInventoryEvent);
|
||||||
SubscribeLocalEvent<InventoryComponent, SlipAttemptEvent>(RelayInventoryEvent);
|
SubscribeLocalEvent<InventoryComponent, SlipAttemptEvent>(RelayInventoryEvent);
|
||||||
SubscribeLocalEvent<InventoryComponent, RefreshMovementSpeedModifiersEvent>(RelayInventoryEvent);
|
SubscribeLocalEvent<InventoryComponent, RefreshMovementSpeedModifiersEvent>(RelayInventoryEvent);
|
||||||
|
SubscribeLocalEvent<InventoryComponent, GetExplosionResistanceEvent>(RelayInventoryEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void RelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, T args) where T : EntityEventArgs, IInventoryRelayEvent
|
protected void RelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, T args) where T : EntityEventArgs, IInventoryRelayEvent
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
admin-explosion-eui-title = Spawn Explosions
|
||||||
|
|
||||||
|
admin-explosion-eui-label-type = Explosion Type
|
||||||
|
admin-explosion-eui-label-mapid = Map ID
|
||||||
|
admin-explosion-eui-label-xmap = X (Map)
|
||||||
|
admin-explosion-eui-label-ymap = Y (Map)
|
||||||
|
admin-explosion-eui-label-current = Current Position
|
||||||
|
admin-explosion-eui-label-preview = Preview
|
||||||
|
admin-explosion-eui-label-total = Total Intensity
|
||||||
|
admin-explosion-eui-label-slope = Intensity Slope
|
||||||
|
admin-explosion-eui-label-max = Max Intensity
|
||||||
|
admin-explosion-eui-label-directional = Directional
|
||||||
|
admin-explosion-eui-label-angle = Angle
|
||||||
|
admin-explosion-eui-label-spread = Spread
|
||||||
|
admin-explosion-eui-label-distance = Distance
|
||||||
|
admin-explosion-eui-label-spawn = Kabloom!
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
- type: Clothing
|
- type: Clothing
|
||||||
sprite: Clothing/Head/Helmets/bombsuit.rsi
|
sprite: Clothing/Head/Helmets/bombsuit.rsi
|
||||||
- type: IngestionBlocker
|
- type: IngestionBlocker
|
||||||
|
- type: ExplosionResistance
|
||||||
|
resistance: 0.25 # +0.65 from body -> 90%
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: ClothingHeadBase
|
parent: ClothingHeadBase
|
||||||
|
|||||||
@@ -8,13 +8,15 @@
|
|||||||
sprite: Clothing/OuterClothing/Suits/bombsuit.rsi
|
sprite: Clothing/OuterClothing/Suits/bombsuit.rsi
|
||||||
- type: Clothing
|
- type: Clothing
|
||||||
sprite: Clothing/OuterClothing/Suits/bombsuit.rsi
|
sprite: Clothing/OuterClothing/Suits/bombsuit.rsi
|
||||||
- type: Armor
|
- type: Armor #explosion damage ignores "normal" armor, see ExplosionResistance.
|
||||||
modifiers:
|
modifiers:
|
||||||
coefficients:
|
coefficients:
|
||||||
Blunt: 0.2 #big blast protection, a little protection from heat, negligable cushioning from other damage cos its thick
|
Blunt: 0.5
|
||||||
Slash: 0.9
|
Slash: 0.9
|
||||||
Piercing: 0.9
|
Piercing: 0.9
|
||||||
Heat: 0.75
|
Heat: 0.75
|
||||||
|
- type: ExplosionResistance
|
||||||
|
resistance: 0.65 # +0.25 from helmet -> 90%
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: ClothingOuterBase
|
parent: ClothingOuterBase
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
- type: entity
|
||||||
|
abstract: true
|
||||||
|
id: ExplosionLight
|
||||||
|
name: explosion light
|
||||||
|
components:
|
||||||
|
- type: PointLight
|
||||||
|
- type: Tag
|
||||||
|
tags:
|
||||||
|
- HideContextMenu
|
||||||
@@ -21,6 +21,12 @@
|
|||||||
layer:
|
layer:
|
||||||
- SmallImpassable
|
- SmallImpassable
|
||||||
- type: Nuke
|
- type: Nuke
|
||||||
|
# ~50 tile radius in open space
|
||||||
|
# close to defaulkt max cap.
|
||||||
|
explosionType: Default
|
||||||
|
maxIntensity: 100
|
||||||
|
intensitySlope: 5
|
||||||
|
totalIntensity: 500000
|
||||||
diskSlot:
|
diskSlot:
|
||||||
name: Disk
|
name: Disk
|
||||||
insertSound:
|
insertSound:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
components:
|
components:
|
||||||
- type: Battery
|
- type: Battery
|
||||||
- type: PowerCell
|
- type: PowerCell
|
||||||
|
- type: Explosive
|
||||||
|
explosionType: Default
|
||||||
- type: Sprite
|
- type: Sprite
|
||||||
netsync: false
|
netsync: false
|
||||||
- type: SolutionContainerManager
|
- type: SolutionContainerManager
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
icon: Interface/Actions/internal0.png
|
icon: Interface/Actions/internal0.png
|
||||||
iconOn: Interface/Actions/internal1.png
|
iconOn: Interface/Actions/internal1.png
|
||||||
event: !type:ToggleActionEvent
|
event: !type:ToggleActionEvent
|
||||||
|
- type: Explosive
|
||||||
|
explosionType: Default
|
||||||
|
maxIntensity: 20
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: GasTankBase
|
parent: GasTankBase
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
damage: {}
|
damage: {}
|
||||||
deleteOnCollide: false
|
deleteOnCollide: false
|
||||||
- type: Explosive
|
- type: Explosive
|
||||||
devastationRange: 3
|
explosionType: Default
|
||||||
heavyImpactRange: 5
|
totalIntensity: 600.0
|
||||||
lightImpactRange: 7
|
intensitySlope: 30
|
||||||
flashRange: 10
|
maxIntensity: 45
|
||||||
- type: Physics
|
- type: Physics
|
||||||
bodyType: Dynamic
|
bodyType: Dynamic
|
||||||
fixedRotation: false
|
fixedRotation: false
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- shape:
|
- shape:
|
||||||
!type:PhysShapeCircle
|
!type:PhysShapeCircle
|
||||||
radius: 0.5
|
radius: 0.8
|
||||||
mass: 200
|
mass: 200
|
||||||
hard: true
|
hard: true
|
||||||
# Didn't use MapGrid for now as the bounds are stuffed.
|
# Didn't use MapGrid for now as the bounds are stuffed.
|
||||||
|
|||||||
@@ -187,10 +187,10 @@
|
|||||||
state: frag
|
state: frag
|
||||||
- type: ExplodeOnTrigger
|
- type: ExplodeOnTrigger
|
||||||
- type: Explosive
|
- type: Explosive
|
||||||
devastationRange: 1
|
explosionType: Default
|
||||||
heavyImpactRange: 2
|
maxIntensity: 40
|
||||||
lightImpactRange: 4
|
intensitySlope: 6
|
||||||
flashRange: 10
|
totalIntensity: 200
|
||||||
- type: PointLight
|
- type: PointLight
|
||||||
radius: 3.5
|
radius: 3.5
|
||||||
color: orange
|
color: orange
|
||||||
@@ -251,10 +251,9 @@
|
|||||||
state: grenade
|
state: grenade
|
||||||
- type: ExplodeOnTrigger
|
- type: ExplodeOnTrigger
|
||||||
- type: Explosive
|
- type: Explosive
|
||||||
devastationRange: 1
|
explosionType: Default
|
||||||
heavyImpactRange: 2
|
totalIntensity: 500 # a ~7 tile radius. This is pretty big.
|
||||||
lightImpactRange: 7
|
maxIntensity: 10 # caps at 150 damage
|
||||||
flashRange: 10
|
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: BulletGrenadeFlash
|
id: BulletGrenadeFlash
|
||||||
@@ -282,10 +281,8 @@
|
|||||||
state: grenade
|
state: grenade
|
||||||
- type: ExplodeOnTrigger
|
- type: ExplodeOnTrigger
|
||||||
- type: Explosive
|
- type: Explosive
|
||||||
devastationRange: 0
|
explosionType: Default
|
||||||
heavyImpactRange: 1
|
totalIntensity: 64 # about a ~4 tile radius
|
||||||
lightImpactRange: 4
|
|
||||||
flashRange: 10
|
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: BulletFoam
|
id: BulletFoam
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
- type: OnUseTimerTrigger
|
- type: OnUseTimerTrigger
|
||||||
delay: 3.5
|
delay: 3.5
|
||||||
- type: Explosive
|
- type: Explosive
|
||||||
devastationRange: 0
|
explosionType: Default
|
||||||
heavyImpactRange: 2
|
maxIntensity: 10
|
||||||
lightImpactRange: 4
|
intensitySlope: 3
|
||||||
flashRange: 7
|
totalIntensity: 120 # about a ~4 tile radius
|
||||||
- type: ExplodeOnTrigger
|
- type: ExplodeOnTrigger
|
||||||
- type: Damageable
|
- type: Damageable
|
||||||
damageContainer: Inorganic
|
damageContainer: Inorganic
|
||||||
@@ -94,10 +94,12 @@
|
|||||||
- type: OnUseTimerTrigger
|
- type: OnUseTimerTrigger
|
||||||
delay: 5
|
delay: 5
|
||||||
- type: Explosive
|
- type: Explosive
|
||||||
devastationRange: 1
|
explosionType: Default
|
||||||
heavyImpactRange: 3
|
# About a 3-3.5 tile radius, strong enough to break reinforced walls near centre.
|
||||||
lightImpactRange: 5
|
# Currently a big spacing hazzard.
|
||||||
flashRange: 10
|
totalIntensity: 800
|
||||||
|
intensitySlope: 15
|
||||||
|
maxIntensity: 45
|
||||||
- type: ExplodeOnTrigger
|
- type: ExplodeOnTrigger
|
||||||
- type: Damageable
|
- type: Damageable
|
||||||
damageContainer: Inorganic
|
damageContainer: Inorganic
|
||||||
@@ -129,9 +131,10 @@
|
|||||||
- type: OnUseTimerTrigger
|
- type: OnUseTimerTrigger
|
||||||
delay: 5
|
delay: 5
|
||||||
- type: Explosive
|
- type: Explosive
|
||||||
devastationRange: 25
|
explosionType: Default
|
||||||
heavyImpactRange: 25
|
totalIntensity: 20000 # ~15 tile radius.
|
||||||
flashRange: 50
|
intensitySlope: 5
|
||||||
|
maxIntensity: 50
|
||||||
- type: ExplodeOnTrigger
|
- type: ExplodeOnTrigger
|
||||||
- type: Damageable
|
- type: Damageable
|
||||||
damageContainer: Inorganic
|
damageContainer: Inorganic
|
||||||
|
|||||||
@@ -35,4 +35,3 @@
|
|||||||
drawdepth: Items
|
drawdepth: Items
|
||||||
noRot: false
|
noRot: false
|
||||||
- type: Pullable
|
- type: Pullable
|
||||||
- type: ExplosionLaunched
|
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
- type: Anchorable
|
- type: Anchorable
|
||||||
- type: Pullable
|
- type: Pullable
|
||||||
- type: AMEController
|
- type: AMEController
|
||||||
|
- type: Explosive
|
||||||
|
explosionType: Default
|
||||||
|
intensitySlope: 5
|
||||||
|
maxIntensity: 60
|
||||||
- type: ActivatableUI
|
- type: ActivatableUI
|
||||||
key: enum.AMEControllerUiKey.Key
|
key: enum.AMEControllerUiKey.Key
|
||||||
- type: UserInterface
|
- type: UserInterface
|
||||||
|
|||||||
@@ -64,10 +64,10 @@
|
|||||||
min: 1
|
min: 1
|
||||||
max: 1
|
max: 1
|
||||||
- type: Explosive
|
- type: Explosive
|
||||||
devastationRange: 1
|
explosionType: Default
|
||||||
heavyImpactRange: 3
|
maxIntensity: 100
|
||||||
lightImpactRange: 5
|
intensitySlope: 2
|
||||||
flashRange: 6
|
totalIntensity: 200
|
||||||
- type: Wires
|
- type: Wires
|
||||||
BoardName: "Substation"
|
BoardName: "Substation"
|
||||||
LayoutId: Substation
|
LayoutId: Substation
|
||||||
|
|||||||
@@ -167,6 +167,8 @@
|
|||||||
state_closed: freezer_door
|
state_closed: freezer_door
|
||||||
- type: AccessReader
|
- type: AccessReader
|
||||||
access: [ [ "Kitchen" ] ]
|
access: [ [ "Kitchen" ] ]
|
||||||
|
- type: ExplosionResistance
|
||||||
|
resistance: 0.90
|
||||||
|
|
||||||
# Botanist
|
# Botanist
|
||||||
- type: entity
|
- type: entity
|
||||||
|
|||||||
@@ -19,12 +19,8 @@
|
|||||||
types:
|
types:
|
||||||
Heat: 10
|
Heat: 10
|
||||||
- type: Explosive
|
- type: Explosive
|
||||||
# The explosive component is independent of the stored liquid :/
|
explosionType: Default
|
||||||
# I dream of a day where all grenades/explosives will be reagent dependent.
|
totalIntensity: 120 # ~ 5 tile radius
|
||||||
devastationRange: 0
|
|
||||||
heavyImpactRange: 2
|
|
||||||
lightImpactRange: 6
|
|
||||||
flashRange: 5
|
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: WeldingFuelTankFull
|
id: WeldingFuelTankFull
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
- SmallImpassable
|
- SmallImpassable
|
||||||
- type: Tag
|
- type: Tag
|
||||||
tags:
|
tags:
|
||||||
- ExplosivePassable
|
|
||||||
- Wooden
|
- Wooden
|
||||||
- type: Damageable
|
- type: Damageable
|
||||||
damageModifierSet: Wood
|
damageModifierSet: Wood
|
||||||
|
|||||||
@@ -25,9 +25,6 @@
|
|||||||
- type: Sprite
|
- type: Sprite
|
||||||
sprite: Structures/Walls/solid.rsi
|
sprite: Structures/Walls/solid.rsi
|
||||||
state: wall_girder
|
state: wall_girder
|
||||||
- type: Tag
|
|
||||||
tags:
|
|
||||||
- ExplosivePassable
|
|
||||||
- type: Damageable
|
- type: Damageable
|
||||||
damageContainer: Inorganic
|
damageContainer: Inorganic
|
||||||
damageModifierSet: Metallic
|
damageModifierSet: Metallic
|
||||||
|
|||||||
@@ -89,13 +89,11 @@
|
|||||||
amount: 1
|
amount: 1
|
||||||
effects:
|
effects:
|
||||||
- !type:ExplosionReactionEffect
|
- !type:ExplosionReactionEffect
|
||||||
#Ranges used when 1 potassium + 1 water react (A unit reaction)
|
explosionType: Default
|
||||||
devastationRange: 0.025
|
maxIntensity: 1 # at most 15 damage per tile
|
||||||
heavyImpactRange: 0.05
|
intensityPerUnit: 0.5 # 50+50 reagent for maximum explosion
|
||||||
lightImpactRange: 0.075
|
intensitySlope: 2
|
||||||
flashRange: 0.1
|
maxTotalIntensity: 25
|
||||||
scaled: true #Scaled proportionally to amount of potassium and water
|
|
||||||
maxScale: 20 #Explosion strength stops scaling at 20 potassium + 20 water
|
|
||||||
|
|
||||||
- type: reaction
|
- type: reaction
|
||||||
id: Smoke
|
id: Smoke
|
||||||
|
|||||||
@@ -37,13 +37,13 @@
|
|||||||
Fluorine:
|
Fluorine:
|
||||||
amount: 3
|
amount: 3
|
||||||
effects:
|
effects:
|
||||||
# TODO electro's pretty explosions PR make this big and firey!!
|
|
||||||
# TODO solution temperature!!
|
# TODO solution temperature!!
|
||||||
- !type:ExplosionReactionEffect
|
- !type:ExplosionReactionEffect
|
||||||
devastationRange: 0
|
explosionType: Default # 15 damage per intensity.
|
||||||
heavyImpactRange: 0
|
maxIntensity: 1 # at most 15 damage per tile.
|
||||||
lightImpactRange: 2
|
intensityPerUnit: 3 # 12 total input reagent units reach max total intensity
|
||||||
scaled: false
|
intensitySlope: 0.5
|
||||||
|
maxTotalIntensity: 9
|
||||||
- !type:PopupMessage
|
- !type:PopupMessage
|
||||||
messages: [ "clf3-explosion" ]
|
messages: [ "clf3-explosion" ]
|
||||||
type: Pvs
|
type: Pvs
|
||||||
|
|||||||
41
Resources/Prototypes/explosion.yml
Normal file
41
Resources/Prototypes/explosion.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Does not currently support prototype hot-reloading. See comments in c# file.
|
||||||
|
|
||||||
|
- type: explosion
|
||||||
|
id: Default
|
||||||
|
damagePerIntensity:
|
||||||
|
types:
|
||||||
|
Heat: 5
|
||||||
|
Blunt: 5
|
||||||
|
Piercing: 5
|
||||||
|
tileBreakChance: [0, 0.5, 1]
|
||||||
|
tileBreakIntensity: [0, 10, 30]
|
||||||
|
tileBreakRerollReduction: 20
|
||||||
|
lightColor: Orange
|
||||||
|
texturePath: /Textures/Effects/fire.rsi
|
||||||
|
fireStates: 3
|
||||||
|
|
||||||
|
- type: explosion
|
||||||
|
id: Radioactive
|
||||||
|
damagePerIntensity:
|
||||||
|
types:
|
||||||
|
Radiation: 5
|
||||||
|
Heat: 4
|
||||||
|
Blunt: 3
|
||||||
|
Piercing: 3
|
||||||
|
lightColor: Green
|
||||||
|
fireColor: Green
|
||||||
|
texturePath: /Textures/Effects/fire_greyscale.rsi
|
||||||
|
fireStates: 3
|
||||||
|
|
||||||
|
- type: explosion
|
||||||
|
id: Cryo
|
||||||
|
damagePerIntensity:
|
||||||
|
types:
|
||||||
|
Cold: 5
|
||||||
|
Blunt: 2
|
||||||
|
tileBreakChance: [0]
|
||||||
|
tileBreakIntensity: [0]
|
||||||
|
lightColor: Blue
|
||||||
|
fireColor: Blue
|
||||||
|
texturePath: /Textures/Effects/fire_greyscale.rsi
|
||||||
|
fireStates: 3
|
||||||
BIN
Resources/Textures/Effects/fire_greyscale.rsi/1.png
Normal file
BIN
Resources/Textures/Effects/fire_greyscale.rsi/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
Resources/Textures/Effects/fire_greyscale.rsi/2.png
Normal file
BIN
Resources/Textures/Effects/fire_greyscale.rsi/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
BIN
Resources/Textures/Effects/fire_greyscale.rsi/3.png
Normal file
BIN
Resources/Textures/Effects/fire_greyscale.rsi/3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
1
Resources/Textures/Effects/fire_greyscale.rsi/meta.json
Normal file
1
Resources/Textures/Effects/fire_greyscale.rsi/meta.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/tgstation/tgstation at 04e43d8c1d5097fdb697addd4395fb849dd341bd", "states": [{"name": "1", "directions": 4, "delays": [[0.1, 0.2, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.1, 0.1, 0.1]]}, {"name": "2", "directions": 4, "delays": [[0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "3", "directions": 4, "delays": [[0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}]}
|
||||||
Reference in New Issue
Block a user