Revert "Explosion refactor TEST MERG (#6995)" (#7005)

This commit is contained in:
Leon Friedrich
2022-03-06 06:02:34 +13:00
committed by GitHub
parent cd1902cdf2
commit c95516e5b2
71 changed files with 766 additions and 3964 deletions

View File

@@ -1,185 +0,0 @@
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 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 != _eyeManager.CurrentMap)
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;
foreach (var (gridId, tileSets) in Tiles)
{
if (!_mapManager.TryGetGrid(gridId, out var grid))
continue;
var gridXform = _entityManager.GetComponent<TransformComponent>(grid.GridEntityId);
gridBounds = gridXform.InvWorldMatrix.TransformBox(args.WorldBounds);
DrawText(handle, gridBounds, gridXform.WorldMatrix, tileSets);
}
if (SpaceTiles == null)
return;
gridBounds = Matrix3.Invert(SpaceMatrix).TransformBox(args.WorldBounds);
DrawText(handle, gridBounds, SpaceMatrix, SpaceTiles);
}
private void DrawText(
DrawingHandleScreen handle,
Box2 gridBounds,
Matrix3 transform,
Dictionary<int, List<Vector2i>> tileSets)
{
for (var i = 1; i < Intensity.Count; i++)
{
if (!tileSets.TryGetValue(i, out var tiles))
continue;
foreach (var tile in tiles)
{
// is the center of this tile visible to the user?
if (!gridBounds.Contains((Vector2) tile + 0.5f))
continue;
var worldCenter = transform.Transform((Vector2) tile + 0.5f);
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);
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;
foreach (var (gridId, tileSets) in Tiles)
{
if (!_mapManager.TryGetGrid(gridId, out var grid))
continue;
var gridXform = _entityManager.GetComponent<TransformComponent>(grid.GridEntityId);
handle.SetTransform(gridXform.WorldMatrix);
gridBounds = gridXform.InvWorldMatrix.TransformBox(args.WorldBounds);
DrawTiles(handle, gridBounds, tileSets);
}
if (SpaceTiles == null)
return;
gridBounds = Matrix3.Invert(SpaceMatrix).TransformBox(args.WorldBounds);
handle.SetTransform(SpaceMatrix);
DrawTiles(handle, gridBounds, SpaceTiles);
}
private void DrawTiles(
DrawingHandleWorld handle,
Box2 gridBounds,
Dictionary<int, List<Vector2i>> tileSets)
{
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)
{
// is the center of this tile visible to the user?
if (!gridBounds.Contains((Vector2) tile + 0.5f))
continue;
var box = Box2.UnitCentered.Translated((Vector2) tile + 0.5f);
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;
}
}

View File

@@ -1,75 +0,0 @@
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 += () => SendMessage(new SpawnExplosionEuiMsg.Close());
}
public override void Opened()
{
base.Opened();
_window.OpenCentered();
}
public override void Closed()
{
base.Closed();
_window.OnClose -= () => SendMessage(new SpawnExplosionEuiMsg.Close());
_window.Close();
ClearOverlay();
}
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;
}
}

View File

@@ -1,46 +0,0 @@
<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>

View File

@@ -1,142 +0,0 @@
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);
}
}

View File

@@ -16,7 +16,6 @@ namespace Content.Client.Entry
"Temperature", "Temperature",
"AtmosExposed", "AtmosExposed",
"Explosive", "Explosive",
"ExplosionResistance",
"Vocal", "Vocal",
"OnUseTimerTrigger", "OnUseTimerTrigger",
"WarpPoint", "WarpPoint",

View File

@@ -1,120 +0,0 @@
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 IEyeManager _eyeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
/// <summary>
/// How intense does the explosion have to be at a tile to advance to the next fire texture state?
/// </summary>
public const int IntensityPerState = 12;
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);
if (ActiveExplosion != null)
{
DrawExplosion(drawHandle, args.WorldBounds, ActiveExplosion, Index);
}
foreach (var exp in CompletedExplosions)
{
DrawExplosion(drawHandle, args.WorldBounds, exp, exp.Intensity.Count);
}
drawHandle.SetTransform(Matrix3.Identity);
}
private void DrawExplosion(DrawingHandleWorld drawHandle, Box2Rotated worldBounds, Explosion exp, int index)
{
if (exp.Map != _eyeManager.CurrentMap)
return;
Box2 gridBounds;
foreach (var (gridId, tiles) in exp.Tiles)
{
if (!_mapManager.TryGetGrid(gridId, out var grid))
continue;
gridBounds = grid.InvWorldMatrix.TransformBox(worldBounds);
drawHandle.SetTransform(grid.WorldMatrix);
DrawTiles(drawHandle, gridBounds, index, tiles, exp);
}
if (exp.SpaceTiles == null)
return;
gridBounds = Matrix3.Invert(exp.SpaceMatrix).TransformBox(worldBounds);
drawHandle.SetTransform(exp.SpaceMatrix);
DrawTiles(drawHandle, gridBounds, index, exp.SpaceTiles, exp);
}
private void DrawTiles(
DrawingHandleWorld drawHandle,
Box2 gridBounds,
int index,
Dictionary<int, List<Vector2i>> tileSets,
Explosion exp)
{
for (var j = 0; j < index; j++)
{
if (!tileSets.TryGetValue(j, out var tiles))
continue;
var frameIndex = (int) Math.Min(exp.Intensity[j] / IntensityPerState, exp.FireFrames.Count - 1);
var frames = exp.FireFrames[frameIndex];
foreach (var tile in tiles)
{
Vector2 bottomLeft = (tile.X, tile.Y);
if (!gridBounds.Contains((bottomLeft + 0.5f) * 1f))
continue;
var texture = _robustRandom.Pick(frames);
drawHandle.DrawTexture(texture, bottomLeft, exp.FireColor);
}
}
}
}

View File

@@ -1,190 +0,0 @@
using Content.Shared.Explosion;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
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!;
/// <summary>
/// For how many seconds should an explosion stay on-screen once it has finished expanding?
/// </summary>
public const float ExplosionPersistence = 0.3f;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<ExplosionEvent>(OnExplosion);
SubscribeNetworkEvent<ExplosionOverlayUpdateEvent>(HandleExplosionUpdate);
var overlayManager = IoCManager.Resolve<IOverlayManager>();
_overlay = new ExplosionOverlay();
if (!overlayManager.HasOverlay<ExplosionOverlay>())
overlayManager.AddOverlay(_overlay);
}
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
foreach (var explosion in _overlay.CompletedExplosions.ToArray())
{
explosion.Lifetime += frameTime;
if (explosion.Lifetime >= ExplosionPersistence)
{
EntityManager.QueueDeleteEntity(explosion.LightEntity);
_overlay.CompletedExplosions.Remove(explosion);
}
}
}
/// <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);
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);
}
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));
return;
}
}
public bool IsNewer(byte explosionId)
{
if (_overlay.ActiveExplosion == null)
return true;
// byte goes up to 255, then wraps back to zero.
// what are the odds of more than 128 explosions happening in a tick?
return _overlay.ActiveExplosion.Explosionid < explosionId
|| _overlay.ActiveExplosion.Explosionid > 192 && explosionId < 64;
}
public override void Shutdown()
{
base.Shutdown();
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (overlayManager.HasOverlay<ExplosionOverlay>())
overlayManager.RemoveOverlay<ExplosionOverlay>();
}
}
internal sealed class Explosion
{
public Dictionary<int, List<Vector2i>>? SpaceTiles;
public Dictionary<GridId, Dictionary<int, List<Vector2i>>> Tiles;
public List<float> Intensity;
public EntityUid LightEntity;
public MapId Map;
public byte Explosionid;
public 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 List<Texture[]> FireFrames = new();
public Color? FireColor;
internal Explosion(ExplosionEvent args, ExplosionPrototype type, EntityUid lightEntity)
{
Map = args.Epicenter.MapId;
SpaceTiles = args.SpaceTiles;
Tiles = args.Tiles;
Intensity = args.Intensity;
SpaceMatrix = args.SpaceMatrix;
Explosionid = args.ExplosionId;
FireColor = type.FireColor;
LightEntity = lightEntity;
var fireRsi = IoCManager.Resolve<IResourceCache>().GetResource<RSIResource>(type.TexturePath).RSI;
foreach (var state in fireRsi)
{
FireFrames.Add(state.GetFrames(RSI.State.Direction.South));
if (FireFrames.Count == type.FireStates)
break;
}
}
}

View File

@@ -162,21 +162,22 @@ namespace Content.Server.AME
{ {
if(_cores.Count < 1 || MasterController == null) { return; } if(_cores.Count < 1 || MasterController == null) { return; }
float radius = 0; var intensity = 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)
{ {
radius += MasterController.InjectionAmount; intensity += MasterController.InjectionAmount;
} }
radius *= 2; intensity = Math.Min(intensity, 8);
radius = Math.Min(radius, 8);
EntitySystem.Get<ExplosionSystem>().TriggerExplosive(MasterController.Owner, radius: radius, delete: false); EntitySystem.Get<ExplosionSystem>().SpawnExplosion(epicenter.Owner, intensity / 2, intensity, intensity * 2, intensity * 3);
} }
} }
} }

View File

@@ -47,7 +47,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 _explosionSystem = default!; [Dependency] private readonly ExplosionSystem _explosions = 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!;
@@ -155,8 +155,8 @@ namespace Content.Server.Administration
verb.Category = VerbCategory.Admin; verb.Category = VerbCategory.Admin;
verb.Act = () => verb.Act = () =>
{ {
var coords = Transform(args.Target).MapPosition; var coords = Transform(args.Target).Coordinates;
Timer.Spawn(_gameTiming.TickPeriod, () => _explosionSystem.QueueExplosion(coords, "Default", 30, 4, 8), CancellationToken.None); Timer.Spawn(_gameTiming.TickPeriod, () => _explosions.SpawnExplosion(coords, 0, 1, 2, 1), CancellationToken.None);
if (TryComp(args.Target, out SharedBodyComponent? body)) if (TryComp(args.Target, out SharedBodyComponent? body))
{ {
body.Gib(); body.Gib();

View File

@@ -1,134 +1,42 @@
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
{ {
public string Command => "explosionui"; [AdminCommand(AdminFlags.Fun)]
public string Description => "Opens a window for easy access to station destruction"; public sealed class ExplosionCommand : IConsoleCommand
public string Help => $"Usage: {Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
var player = shell.Player as IPlayerSession; public string Command => "explode";
if (player == null) public string Description => "Train go boom";
{ public string Help => "Usage: explode <x> <y> <dev> <heavy> <light> <flash>\n" +
shell.WriteLine("This does not work from the server console."); "The explosion happens on the same map as the user.";
return;
}
var eui = IoCManager.Resolve<EuiManager>(); public void Execute(IConsoleShell shell, string argStr, string[] args)
var ui = new SpawnExplosionEui(); {
eui.OpenEui(ui, player); var player = shell.Player as IPlayerSession;
} if (player?.AttachedEntity is not {Valid: true} playerEntity)
} {
shell.WriteLine("You must have an attached entity.");
[AdminCommand(AdminFlags.Fun)] // for the admin. Not so much for anyone else. return;
public sealed class ExplosionCommand : IConsoleCommand }
{
public string Command => "explosion"; var x = float.Parse(args[0]);
public string Description => "Train go boom"; var y = float.Parse(args[1]);
// Note that if you change the arguments, you should also update the client-side SpawnExplosionWindow, as that just var dev = int.Parse(args[2]);
// uses this command. var hvy = int.Parse(args[3]);
public string Help => "Usage: explosion intensity [slope] [maxIntensity] [x y] [mapId] [prototypeId]"; var lgh = int.Parse(args[4]);
var fla = int.Parse(args[5]);
public void Execute(IConsoleShell shell, string argStr, string[] args)
{ var mapTransform = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(playerEntity).GetMapTransform();
if (args.Length == 0 || args.Length == 4 || args.Length > 7) var coords = new EntityCoordinates(mapTransform.Owner, x, y);
{
shell.WriteLine("Wrong number of arguments."); EntitySystem.Get<ExplosionSystem>().SpawnExplosion(coords, dev, hvy, lgh, fla);
return; }
}
if (!float.TryParse(args[0], out var intensity))
{
shell.WriteLine($"Failed to parse intensity: {args[0]}");
return;
}
float slope = 5;
if (args.Length > 1 && !float.TryParse(args[1], out slope))
{
shell.WriteLine($"Failed to parse float: {args[1]}");
return;
}
float maxIntensity = 100;
if (args.Length > 2 && !float.TryParse(args[2], out maxIntensity))
{
shell.WriteLine($"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.WriteLine($"Failed to parse coordinates: {(args[3], args[4])}");
return;
}
}
MapCoordinates coords;
if (args.Length > 5)
{
if (!int.TryParse(args[5], out var parsed))
{
shell.WriteLine($"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.WriteLine($"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.WriteLine($"Unknown explosion prototype: {args[6]}");
return;
}
}
else
{
// no prototype was specified, so lets default to whichever one was defined first
var types = protoMan.EnumeratePrototypes<ExplosionPrototype>();
if (!types.Any())
return;
type = types.First();
}
EntitySystem.Get<ExplosionSystem>().QueueExplosion(coords, type.ID, intensity, slope, maxIntensity);
} }
} }

View File

@@ -1,39 +0,0 @@
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));
}
}

View File

@@ -245,14 +245,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>().TriggerExplosive(Owner, radius: range); EntitySystem.Get<ExplosionSystem>().SpawnExplosion(Owner, (int) (range * 0.25f), (int) (range * 0.5f), (int) (range * 1.5f), 1);
_entMan.QueueDeleteEntity(Owner);
return; return;
} }

View File

@@ -1,5 +1,4 @@
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;
@@ -15,7 +14,6 @@ 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()
{ {
@@ -106,7 +104,6 @@ namespace Content.Server.Atmos.EntitySystems
if (!gridId.IsValid()) if (!gridId.IsValid())
return; return;
_explosionSystem.UpdateAirtightMap(gridId, pos);
_atmosphereSystem.UpdateAdjacent(gridId, pos); _atmosphereSystem.UpdateAdjacent(gridId, pos);
_atmosphereSystem.InvalidateTile(gridId, pos); _atmosphereSystem.InvalidateTile(gridId, pos);

View File

@@ -1,68 +1,78 @@
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 Content.Shared.Explosion; using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Chemistry.ReactionEffects namespace Content.Server.Chemistry.ReactionEffects
{ {
[DataDefinition] [DataDefinition]
public class ExplosionReactionEffect : ReagentEffect public sealed class ExplosionReactionEffect : ReagentEffect
{ {
/// <summary> [DataField("devastationRange")]
/// The type of explosion. Determines damage types and tile break chance scaling.
/// </summary>
[DataField("explosionType", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))]
[JsonIgnore] [JsonIgnore]
public string ExplosionType = default!; private float _devastationRange = 1;
[DataField("heavyImpactRange")]
[JsonIgnore]
private float _heavyImpactRange = 2;
[DataField("lightImpactRange")]
[JsonIgnore]
private float _lightImpactRange = 3;
[DataField("flashRange")]
[JsonIgnore]
private float _flashRange;
/// <summary> /// <summary>
/// The max intensity the explosion can have at a given tile. Places an upper limit of damage and tile break /// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount.
/// chance.
/// </summary> /// </summary>
[DataField("maxIntensity")] [DataField("scaled")]
[JsonIgnore] [JsonIgnore]
public float MaxIntensity = 5; private bool _scaled;
/// <summary> /// <summary>
/// How quickly intensity drops off as you move away from the epicenter /// Maximum scaling on ranges. For example, if it equals 5, then it won't scaled anywhere past
/// 5 times the minimum reactant amount.
/// </summary> /// </summary>
[DataField("intensitySlope")] [DataField("maxScale")]
[JsonIgnore] [JsonIgnore]
public float IntensitySlope = 1; private float _maxScale = 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 intensity = MathF.Min((float) args.Quantity * IntensityPerUnit, MaxTotalIntensity); var floatIntensity = (float) args.Quantity;
EntitySystem.Get<ExplosionSystem>().QueueExplosion( if (!args.EntityManager.HasComponent<SolutionContainerManagerComponent>(args.SolutionEntity))
args.SolutionEntity, return;
ExplosionType,
intensity, //Handle scaling
IntensitySlope, if (_scaled)
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);
} }
} }
} }

View File

@@ -1,17 +1,13 @@
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
{ {
@@ -54,40 +50,6 @@ 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.

View File

@@ -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.TriggerExplosive(owner); system.ExplosionSystem.SpawnExplosion(owner);
} }
} }
} }

View File

@@ -1,8 +1,40 @@
using Content.Server.Throwing;
using Content.Shared.Acts;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Server.Explosion.Components; namespace Content.Server.Explosion.Components
// TODO EXPLOSION make this a tag? or just get rid of it and launch all unanchored physics entities?
[RegisterComponent]
public sealed class ExplosionLaunchedComponent : Component
{ {
[RegisterComponent]
public sealed class ExplosionLaunchedComponent : Component, IExAct
{
[Dependency] private readonly IEntityManager _entMan = 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,
};
Owner.TryThrow(direction, throwForce);
}
}
} }

View File

@@ -1,30 +0,0 @@
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();
}

View File

@@ -1,59 +1,34 @@
using Content.Server.Explosion.EntitySystems; using Content.Server.Chemistry.ReactionEffects;
using Content.Shared.Explosion; using Content.Server.Destructible.Thresholds.Behaviors;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.GameObjects;
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
{ {
public override string Name => "Explosive";
/// <summary> /// <summary>
/// The explosion prototype. This determines the damage types, the tile-break chance, and some visual /// Specifies an explosion range should this entity be exploded.
/// 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 = 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>
/// This number can be overridden by passing optional argument to <see /// Explosions can be caused by:
/// cref="ExplosionSystem.TriggerExplosive"/>. /// <list type="bullet">
/// <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>
[ViewVariables(VVAccess.ReadWrite)] [RegisterComponent]
[DataField("totalIntensity")] public sealed class ExplosiveComponent : Component
public float TotalIntensity = 10; {
[DataField("devastationRange")]
public int DevastationRange;
[DataField("heavyImpactRange")]
public int HeavyImpactRange;
[DataField("lightImpactRange")]
public int LightImpactRange;
[DataField("flashRange")]
public int FlashRange;
/// <summary> public bool Exploding { get; set; } = false;
/// Avoid somehow double-triggering this explosion (e.g. by damaging this entity from it's own explosion. }
/// </summary>
public bool Exploded;
} }

View File

@@ -1,141 +0,0 @@
using Content.Server.Atmos.Components;
using Content.Server.Destructible;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.Explosion;
using Content.Shared.FixedPoint;
using Robust.Shared.Map;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class ExplosionSystem : EntitySystem
{
[Dependency] private readonly DestructibleSystem _destructibleSystem = default!;
// The explosion intensity required to break an entity depends on the explosion type. So it is stored in a
// Dictionary<string, float>
//
// Hence, each tile has a tuple (Dictionary<string, float>, AtmosDirection). This specifies what directions are
// blocked, and how intense a given explosion type needs to be in order to destroy ALL airtight entities on that
// tile. This is the TileData struct.
//
// We then need this data for every tile on a grid. So this mess of a variable maps the Grid ID and Vector2i grid
// indices to this tile-data struct.
private Dictionary<GridId, Dictionary<Vector2i, TileData>> _airtightMap = new();
public void UpdateAirtightMap(GridId gridId, Vector2i tile)
{
if (_mapManager.TryGetGrid(gridId, out var grid))
UpdateAirtightMap(grid, tile);
}
/// <summary>
/// Update the map of explosion blockers.
/// </summary>
/// <remarks>
/// Gets a list of all airtight entities on a tile. Assembles a <see cref="AtmosDirection"/> that specifies
/// what directions are blocked, along with the largest explosion tolerance. Note that as we only keep track
/// of the largest tolerance, this means that the explosion map will actually be inaccurate if you have
/// something like a normal and a reinforced windoor on the same tile. But given that this is a pretty rare
/// occurrence, I am fine with this.
/// </remarks>
public void UpdateAirtightMap(IMapGrid grid, Vector2i tile)
{
Dictionary<string, float> tolerance = new();
var blockedDirections = AtmosDirection.Invalid;
if (!_airtightMap.ContainsKey(grid.Index))
_airtightMap[grid.Index] = new();
foreach (var uid in grid.GetAnchoredEntities(tile))
{
if (!EntityManager.TryGetComponent(uid, out AirtightComponent? airtight) || !airtight.AirBlocked)
continue;
blockedDirections |= airtight.AirBlockedDirection;
foreach (var (type, value) in GetExplosionTolerance(uid))
{
if (!tolerance.TryAdd(type, value))
tolerance[type] = Math.Max(tolerance[type], value);
}
}
if (blockedDirections != AtmosDirection.Invalid)
_airtightMap[grid.Index][tile] = new(tolerance, blockedDirections);
else
_airtightMap[grid.Index].Remove(tile);
}
/// <summary>
/// On receiving damage, re-evaluate how much explosion damage is needed to destroy an airtight entity.
/// </summary>
private void OnAirtightDamaged(EntityUid uid, AirtightComponent airtight, DamageChangedEvent args)
{
// do we need to update our explosion blocking map?
if (!airtight.AirBlocked)
return;
if (!EntityManager.TryGetComponent(uid, out TransformComponent transform) || !transform.Anchored)
return;
if (!_mapManager.TryGetGrid(transform.GridID, out var grid))
return;
UpdateAirtightMap(grid, grid.CoordinatesToTile(transform.Coordinates));
}
/// <summary>
/// Return a dictionary that specifies how intense a given explosion type needs to be in order to destroy an entity.
/// </summary>
public Dictionary<string, float> GetExplosionTolerance(EntityUid uid)
{
// How much total damage is needed to destroy this entity? This also includes "break" behaviors. This ASSUMES
// that this will result in a non-airtight entity.Entities that ONLY break via construction graph node changes
// are currently effectively "invincible" as far as this is concerned. This really should be done more rigorously.
var totalDamageTarget = _destructibleSystem.DestroyedAt(uid);
Dictionary<string, float> explosionTolerance = new();
if (totalDamageTarget == FixedPoint2.MaxValue || !TryComp(uid, out DamageableComponent? damageable))
return explosionTolerance;
// What multiple of each explosion type damage set will result in the damage exceeding the required amount? This
// does not support entities dynamically changing explosive resistances (e.g. via clothing). But these probably
// shouldn't be airtight structures anyways....
foreach (var explosionType in _prototypeManager.EnumeratePrototypes<ExplosionPrototype>())
{
// evaluate the damage that this damage type would do to this entity
var damagePerIntensity = FixedPoint2.Zero;
foreach (var (type, value) in explosionType.DamagePerIntensity.DamageDict)
{
if (!damageable.Damage.DamageDict.ContainsKey(type))
continue;
var ev = new GetExplosionResistanceEvent(explosionType.ID);
RaiseLocalEvent(uid, ev, false);
damagePerIntensity += value * Math.Clamp(0, 1 - ev.Resistance, 1);
}
explosionTolerance[explosionType.ID] = (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity);
}
return explosionTolerance;
}
}
/// <summary>
/// Data struct that describes the explosion-blocking airtight entities on a tile.
/// </summary>
internal struct TileData
{
public TileData(Dictionary<string, float> explosionTolerance, AtmosDirection blockedDirections)
{
ExplosionTolerance = explosionTolerance;
BlockedDirections = blockedDirections;
}
public Dictionary<string, float> ExplosionTolerance;
public AtmosDirection BlockedDirections = AtmosDirection.Invalid;
}

View File

@@ -1,438 +0,0 @@
using Content.Shared.Atmos;
using Robust.Shared.Map;
namespace Content.Server.Explosion.EntitySystems;
// This partial part of the explosion system has all of the functions used to facilitate explosions moving across grids.
// A good portion of it is focused around keeping track of what tile-indices on a grid correspond to tiles that border
// space. AFAIK no other system currently needs to track these "edge-tiles". If they do, this should probably be a
// property of the grid itself?
public sealed partial class ExplosionSystem : EntitySystem
{
/// <summary>
/// Set of tiles of each grid that are directly adjacent to space, along with the directions that face space.
/// </summary>
private Dictionary<GridId, Dictionary<Vector2i, AtmosDirection>> _gridEdges = new();
/// <summary>
/// Set of tiles of each grid that are diagonally adjacent to space
/// </summary>
private Dictionary<GridId, HashSet<Vector2i>> _diagGridEdges = new();
/// <summary>
/// On grid startup, prepare a map of grid edges.
/// </summary>
private void OnGridStartup(GridStartupEvent ev)
{
if (!_mapManager.TryGetGrid(ev.GridId, out var grid))
return;
Dictionary<Vector2i, AtmosDirection> edges = new();
HashSet<Vector2i> diagEdges = new();
_gridEdges[ev.GridId] = edges;
_diagGridEdges[ev.GridId] = diagEdges;
foreach (var tileRef in grid.GetAllTiles())
{
if (tileRef.Tile.IsEmpty)
continue;
if (IsEdge(grid, tileRef.GridIndices, out var dir))
edges.Add(tileRef.GridIndices, dir);
else if (IsDiagonalEdge(grid, tileRef.GridIndices))
diagEdges.Add(tileRef.GridIndices);
}
}
private void OnGridRemoved(GridRemovalEvent ev)
{
_airtightMap.Remove(ev.GridId);
_gridEdges.Remove(ev.GridId);
_diagGridEdges.Remove(ev.GridId);
}
/// <summary>
/// Take our map of grid edges, where each is defined in their own grid's reference frame, and map those
/// edges all onto one grids reference frame.
/// </summary>
public (Dictionary<Vector2i, GridBlockData>, ushort) TransformGridEdges(MapId targetMap, GridId? referenceGrid, List<GridId> localGrids)
{
Dictionary<Vector2i, GridBlockData> transformedEdges = new();
var targetMatrix = Matrix3.Identity;
Angle targetAngle = new();
ushort tileSize = DefaultTileSize;
// if the explosion is centered on some grid (and not just space), get the transforms.
if (referenceGrid != null)
{
var targetGrid = _mapManager.GetGrid(referenceGrid.Value);
var xform = Transform(targetGrid.GridEntityId);
targetAngle = xform.WorldRotation;
targetMatrix = xform.InvWorldMatrix;
tileSize = targetGrid.TileSize;
}
var offsetMatrix = Matrix3.Identity;
offsetMatrix.R0C2 = tileSize / 2;
offsetMatrix.R1C2 = tileSize / 2;
// here we will get a triple nested for loop:
// foreach other grid
// foreach edge tile in that grid
// foreach tile in our grid that touches that tile (vast majority of the time: 1 tile, but could be up to 4)
HashSet<Vector2i> transformedTiles = new();
foreach (var gridToTransform in localGrids)
{
// we treat the target grid separately
if (gridToTransform == referenceGrid)
continue;
if (!_gridEdges.TryGetValue(gridToTransform, out var edges))
continue;
if (!_mapManager.TryGetGrid(gridToTransform, out var grid) ||
grid.ParentMapId != targetMap)
continue;
if (grid.TileSize != tileSize)
{
Logger.Error($"Explosions do not support grids with different grid sizes. GridIds: {gridToTransform} and {referenceGrid}");
continue;
}
var xform = EntityManager.GetComponent<TransformComponent>(grid.GridEntityId);
var matrix = offsetMatrix * xform.WorldMatrix * targetMatrix;
var angle = xform.WorldRotation - targetAngle;
var (x, y) = angle.RotateVec((tileSize / 4, tileSize / 4));
foreach (var (tile, dir) in edges)
{
var center = matrix.Transform(tile);
// this tile might touch several other tiles, or maybe just one tile. Here we use a Vector2i HashSet to
// remove duplicates.
transformedTiles.Clear();
transformedTiles.Add(new((int) MathF.Floor(center.X + x), (int) MathF.Floor(center.Y + y))); // initial direction
transformedTiles.Add(new((int) MathF.Floor(center.X - y), (int) MathF.Floor(center.Y + x))); // rotated 90 degrees
transformedTiles.Add(new((int) MathF.Floor(center.X - x), (int) MathF.Floor(center.Y - y))); // rotated 180 degrees
transformedTiles.Add(new((int) MathF.Floor(center.X + y), (int) MathF.Floor(center.Y - x))); // rotated 270 degrees
foreach (var newIndices in transformedTiles)
{
if (!transformedEdges.TryGetValue(newIndices, out var data))
{
data = new();
transformedEdges[newIndices] = data;
}
data.BlockingGridEdges.Add(new(tile, gridToTransform, center, angle, tileSize));
}
}
}
// Next we transform any diagonal edges.
Vector2i newIndex;
foreach (var gridToTransform in localGrids)
{
// we treat the target grid separately
if (gridToTransform == referenceGrid)
continue;
if (!_diagGridEdges.TryGetValue(gridToTransform, out var diagEdges))
continue;
if (!_mapManager.TryGetGrid(gridToTransform, out var grid) ||
grid.ParentMapId != targetMap)
continue;
if (grid.TileSize != tileSize)
{
Logger.Error($"Explosions do not support grids with different grid sizes. GridIds: {gridToTransform} and {referenceGrid}");
continue;
}
var xform = EntityManager.GetComponent<TransformComponent>(grid.GridEntityId);
var matrix = offsetMatrix * xform.WorldMatrix * targetMatrix;
var angle = xform.WorldRotation - targetAngle;
foreach (var tile in diagEdges)
{
var center = matrix.Transform(tile);
newIndex = new((int) MathF.Floor(center.X), (int) MathF.Floor(center.Y));
if (!transformedEdges.TryGetValue(newIndex, out var data))
{
data = new();
transformedEdges[newIndex] = data;
}
// explosions are not allowed to propagate diagonally ONTO grids. so we just use defaults for some fields.
data.BlockingGridEdges.Add(new(default, null, center, angle, tileSize));
}
}
if (referenceGrid == null)
return (transformedEdges, tileSize);
// finally, we also include the blocking tiles from the reference grid.
if (_gridEdges.TryGetValue(referenceGrid.Value, out var localEdges))
{
foreach (var (tile, _) in localEdges)
{
// grids cannot overlap, so tile should NEVER be an existing entry.
var data = new GridBlockData();
transformedEdges[tile] = data;
data.UnblockedDirections = AtmosDirection.Invalid; // all directions are blocked automatically.
data.BlockingGridEdges.Add(new(tile, referenceGrid.Value, ((Vector2) tile + 0.5f) * tileSize, 0, tileSize));
}
}
if (_diagGridEdges.TryGetValue(referenceGrid.Value, out var localDiagEdges))
{
foreach (var tile in localDiagEdges)
{
// grids cannot overlap, so tile should NEVER be an existing entry.
var data = new GridBlockData();
transformedEdges[tile] = data;
data.UnblockedDirections = AtmosDirection.Invalid; // all directions are blocked automatically.
data.BlockingGridEdges.Add(new(default, null, ((Vector2) tile + 0.5f) * tileSize, 0, tileSize));
}
}
return (transformedEdges, tileSize);
}
/// <summary>
/// Given an grid-edge blocking map, check if the blockers are allowed to propagate to each other through gaps in grids.
/// </summary>
/// <remarks>
/// After grid edges were transformed into the reference frame of some other grid, this function figures out
/// which of those edges are actually blocking explosion propagation.
/// </remarks>
public void GetUnblockedDirections(Dictionary<Vector2i, GridBlockData> transformedEdges, ushort tileSize)
{
foreach (var (tile, data) in transformedEdges)
{
if (data.UnblockedDirections == AtmosDirection.Invalid)
continue; // already all blocked.
var tileCenter = ((Vector2) tile + 0.5f) * tileSize;
foreach (var edge in data.BlockingGridEdges)
{
// if a blocking edge contains the center of the tile, block all directions
if (edge.Box.Contains(tileCenter))
{
data.UnblockedDirections = AtmosDirection.Invalid;
break;
}
// check north
if (edge.Box.Contains(tileCenter + (0, tileSize / 2)))
data.UnblockedDirections &= ~AtmosDirection.North;
// check south
if (edge.Box.Contains(tileCenter + (0, -tileSize / 2)))
data.UnblockedDirections &= ~AtmosDirection.South;
// check east
if (edge.Box.Contains(tileCenter + (tileSize / 2, 0)))
data.UnblockedDirections &= ~AtmosDirection.East;
// check west
if (edge.Box.Contains(tileCenter + (-tileSize / 2, 0)))
data.UnblockedDirections &= ~AtmosDirection.West;
}
}
}
/// <summary>
/// When a tile is updated, we might need to update the grid edge maps.
/// </summary>
private void OnTileChanged(object? sender, TileChangedEventArgs e)
{
// only need to update the grid-edge map if the tile changed from space to not-space.
if (!e.NewTile.Tile.IsEmpty && !e.OldTile.IsEmpty)
return;
var tileRef = e.NewTile;
if (!_mapManager.TryGetGrid(tileRef.GridIndex, out var grid))
return;
if (!_gridEdges.TryGetValue(tileRef.GridIndex, out var edges))
{
edges = new();
_gridEdges[tileRef.GridIndex] = edges;
}
if (!_diagGridEdges.TryGetValue(tileRef.GridIndex, out var diagEdges))
{
diagEdges = new();
_diagGridEdges[tileRef.GridIndex] = diagEdges;
}
if (tileRef.Tile.IsEmpty)
{
// if the tile is empty, it cannot itself be an edge tile.
edges.Remove(tileRef.GridIndices);
diagEdges.Remove(tileRef.GridIndices);
// add any valid neighbours to the list of edge-tiles
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
var neighbourIndex = tileRef.GridIndices.Offset(direction);
if (grid.TryGetTileRef(neighbourIndex, out var neighbourTile) && !neighbourTile.Tile.IsEmpty)
{
edges[neighbourIndex] = edges.GetValueOrDefault(neighbourIndex) | direction.GetOpposite();
diagEdges.Remove(neighbourIndex);
}
}
foreach (var diagNeighbourIndex in GetDiagonalNeighbors(tileRef.GridIndices))
{
if (edges.ContainsKey(diagNeighbourIndex))
continue;
if (grid.TryGetTileRef(diagNeighbourIndex, out var neighbourIndex) && !neighbourIndex.Tile.IsEmpty)
diagEdges.Add(diagNeighbourIndex);
}
return;
}
// the tile is not empty space, but was previously. So update directly adjacent neighbours, which may no longer
// be edge tiles.
AtmosDirection spaceDir;
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
var neighbourIndex = tileRef.GridIndices.Offset(direction);
if (edges.TryGetValue(neighbourIndex, out spaceDir))
{
spaceDir = spaceDir & ~direction.GetOpposite();
if (spaceDir != AtmosDirection.Invalid)
edges[neighbourIndex] = spaceDir;
else
{
// no longer a direct edge ...
edges.Remove(neighbourIndex);
// ... but it could now be a diagonal edge
if (IsDiagonalEdge(grid, neighbourIndex, tileRef.GridIndices))
diagEdges.Add(neighbourIndex);
}
}
}
// and again for diagonal neighbours
foreach (var neighborIndex in GetDiagonalNeighbors(tileRef.GridIndices))
{
if (diagEdges.Contains(neighborIndex) && !IsDiagonalEdge(grid, neighborIndex, tileRef.GridIndices))
diagEdges.Remove(neighborIndex);
}
// finally check if the new tile is itself an edge tile
if (IsEdge(grid, tileRef.GridIndices, out spaceDir))
edges.Add(tileRef.GridIndices, spaceDir);
else if (IsDiagonalEdge(grid, tileRef.GridIndices))
diagEdges.Add(tileRef.GridIndices);
}
/// <summary>
/// Check whether a tile is on the edge of a grid (i.e., whether it borders space).
/// </summary>
/// <remarks>
/// Optionally ignore a specific Vector2i. Used by <see cref="OnTileChanged"/> when we already know that a
/// given tile is not space. This avoids unnecessary TryGetTileRef calls.
/// </remarks>
private bool IsEdge(IMapGrid grid, Vector2i index, out AtmosDirection spaceDirections)
{
spaceDirections = AtmosDirection.Invalid;
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
if (!grid.TryGetTileRef(index.Offset(direction), out var neighborTile) || neighborTile.Tile.IsEmpty)
spaceDirections |= direction;
}
return spaceDirections != AtmosDirection.Invalid;
}
private bool IsDiagonalEdge(IMapGrid grid, Vector2i index, Vector2i? ignore = null)
{
foreach (var neighbourIndex in GetDiagonalNeighbors(index))
{
if (neighbourIndex == ignore)
continue;
if (!grid.TryGetTileRef(neighbourIndex, out var neighborTile) || neighborTile.Tile.IsEmpty)
return true;
}
return false;
}
/// <summary>
/// Enumerate over diagonally adjacent tiles.
/// </summary>
internal static IEnumerable<Vector2i> GetDiagonalNeighbors(Vector2i pos)
{
yield return pos + (1, 1);
yield return pos + (-1, -1);
yield return pos + (1, -1);
yield return pos + (-1, 1);
}
}
public struct GridEdgeData : IEquatable<GridEdgeData>
{
public Vector2i Tile;
public GridId? Grid;
public Box2Rotated Box;
public GridEdgeData(Vector2i tile, GridId? grid, Vector2 center, Angle angle, float size)
{
Tile = tile;
Grid = grid;
Box = new(Box2.CenteredAround(center, (size, size)), angle, center);
}
/// <inheritdoc />
public bool Equals(GridEdgeData other)
{
return Tile.Equals(other.Tile) && Grid.Equals(other.Grid);
}
/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
return (Tile.GetHashCode() * 397) ^ Grid.GetHashCode();
}
}
}
public record GridBlockData
{
/// <summary>
/// What directions of this tile are not blocked by some other grid?
/// </summary>
public AtmosDirection UnblockedDirections = AtmosDirection.All;
/// <summary>
/// Hashset contains information about the edge-tiles, which belong to some other grid(s), that are blocking
/// this tile.
/// </summary>
public HashSet<GridEdgeData> BlockingGridEdges = new();
}

View File

@@ -1,569 +0,0 @@
using System.Linq;
using Content.Server.Explosion.Components;
using Content.Server.Throwing;
using Content.Shared.Damage;
using Content.Shared.Explosion;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class ExplosionSystem : EntitySystem
{
/// <summary>
/// Used to identify explosions when communicating with the client. Might be needed if more than one explosion is spawned in a single tick.
/// </summary>
/// <remarks>
/// Overflowing back to 0 should cause no issue, as long as you don't have more than 256 explosions happening in a single tick.
/// </remarks>
private byte _explosionCounter = 0;
// maybe should just use a UID/explosion-entity and a state to convey information?
// but then need to ignore PVS? Eeehh this works well enough for now.
/// <summary>
/// Arbitrary definition for when an explosion is large enough to require separating the area/tile-finding and
/// the processing into separate ticks.
/// </summary>
/// <remarks>
/// Only used when the explosion processing is not limited by time.
/// </remarks>
public const int NukeArea = 400;
/// <summary>
/// Used to limit explosion processing time. See <see cref="MaxProcessingTime"/>.
/// </summary>
internal readonly Stopwatch Stopwatch = new();
/// <summary>
/// How many tiles to explode before checking the stopwatch timer
/// </summary>
internal static int TileCheckIteration = 1;
/// <summary>
/// Queue for delayed processing of explosions. If there is an explosion that covers more than <see
/// cref="TilesPerTick"/> tiles, other explosions will actually be delayed slightly. Unless it's a station
/// nuke, this delay should never really be noticeable.
/// </summary>
private Queue<Func<Explosion?>> _explosionQueue = new();
/// <summary>
/// The explosion currently being processed.
/// </summary>
private Explosion? _activeExplosion;
/// <summary>
/// While processing an explosion, the "progress" is sent to clients, so that the explosion fireball effect
/// syncs up with the damage. When the tile iteration increments, an update needs to be sent to clients.
/// This integer keeps track of the last value sent to clients.
/// </summary>
private int _previousTileIteration;
/// <summary>
/// Process the explosion queue.
/// </summary>
public override void Update(float frameTime)
{
if (_activeExplosion == null && _explosionQueue.Count == 0)
// nothing to do
return;
Stopwatch.Restart();
var x = Stopwatch.Elapsed.TotalMilliseconds;
var availableTime = MaxProcessingTime;
var tilesRemaining = TilesPerTick;
while (tilesRemaining > 0 && MaxProcessingTime > Stopwatch.Elapsed.TotalMilliseconds)
{
// if there is no active explosion, get a new one to process
if (_activeExplosion == null)
{
// EXPLOSION TODO allow explosion spawning to be interrupted by time limit. In the meantime, ensure that
// there is at-least 1ms of time left before creating a new explosion
if (MathF.Max(MaxProcessingTime - 1, 0.1f) < Stopwatch.Elapsed.TotalMilliseconds)
break;
if (!_explosionQueue.TryDequeue(out var spawnNextExplosion))
break;
_activeExplosion = spawnNextExplosion();
// explosion spawning can be null if something somewhere went wrong. (e.g., negative explosion
// intensity).
if (_activeExplosion == null)
continue;
_explosionCounter++;
_previousTileIteration = 0;
// just a lil nap
if (SleepNodeSys)
{
_nodeGroupSystem.Snoozing = true;
// snooze grid-chunk regeneration?
// snooze power network (recipients look for new suppliers as wires get destroyed).
}
// if this is a single-tick explosion (i.e., not severely limited by number of tiles per tick or
// processing time, we want to process large explosion on a tick separate from the one they were
// generated on.
if (_activeExplosion.Area > NukeArea
&& MaxProcessingTime >= _gameTiming.TickPeriod.TotalMilliseconds)
{
// start processing next turn.
break;
}
}
var processed = _activeExplosion.Proccess(tilesRemaining);
tilesRemaining -= processed;
// has the explosion finished processing?
if (_activeExplosion.FinishedProcessing)
_activeExplosion = null;
}
Logger.InfoS("Explosion", $"Processed {TilesPerTick - tilesRemaining} tiles in {Stopwatch.Elapsed.TotalMilliseconds}ms");
// we have finished processing our tiles. Is there still an ongoing explosion?
if (_activeExplosion != null)
{
// update the client explosion overlays. This ensures that the fire-effects sync up with the entities currently being damaged.
if (_previousTileIteration == _activeExplosion.CurrentIteration)
return;
_previousTileIteration = _activeExplosion.CurrentIteration;
RaiseNetworkEvent(new ExplosionOverlayUpdateEvent(_explosionCounter, _previousTileIteration + 1));
return;
}
if (_explosionQueue.Count > 0)
return;
// We have finished processing all explosions. Clear client explosion overlays
RaiseNetworkEvent(new ExplosionOverlayUpdateEvent(_explosionCounter, int.MaxValue));
//wakey wakey
_nodeGroupSystem.Snoozing = false;
}
/// <summary>
/// Determines whether an entity is blocking a tile or not. (whether it can prevent the tile from being uprooted
/// by an explosion).
/// </summary>
/// <remarks>
/// Used for a variation of <see cref="TurfHelpers.IsBlockedTurf()"/> that makes use of the fact that we have
/// already done an entity lookup on a tile, and don't need to do so again.
/// </remarks>
public bool IsBlockingTurf(EntityUid uid)
{
if (EntityManager.IsQueuedForDeletion(uid))
return false;
if (!TryComp(uid, out IPhysBody? body))
return false;
return body.CanCollide && body.Hard && (body.CollisionLayer & (int) CollisionGroup.Impassable) != 0;
}
/// <summary>
/// Find entities on a grid tile using the EntityLookupComponent and apply explosion effects.
/// </summary>
/// <returns>True if the underlying tile can be uprooted, false if the tile is blocked by a dense entity</returns>
internal bool ExplodeTile(EntityLookupComponent lookup,
IMapGrid grid,
Vector2i tile,
float intensity,
float throwForce,
DamageSpecifier damage,
MapCoordinates epicenter,
HashSet<EntityUid> processed,
string id)
{
var gridBox = new Box2(tile * grid.TileSize, (tile + 1) * grid.TileSize);
// get the entities on a tile. Note that we cannot process them directly, or we get
// enumerator-changed-while-enumerating errors.
List<EntityUid> list = new();
_entityLookup.FastEntitiesIntersecting(lookup, ref gridBox, entity => list.Add(entity));
// process those entities
foreach (var entity in list)
{
ProcessEntity(entity, epicenter, processed, damage, throwForce, id, false);
}
// process anchored entities
var tileBlocked = false;
foreach (var entity in grid.GetAnchoredEntities(tile).ToList())
{
ProcessEntity(entity, epicenter, processed, damage, throwForce, id, true);
tileBlocked |= IsBlockingTurf(entity);
}
// Next, we get the intersecting entities AGAIN, but purely for throwing. This way, glass shards spawned from
// windows will be flung outwards, and not stay where they spawned. This is however somewhat unnecessary, and a
// prime candidate for computational cost-cutting. Alternatively, it would be nice if there was just some sort
// of spawned-on-destruction event that could be used to automatically assemble a list of new entities that need
// to be thrown.
//
// All things considered, until entity spawning & destruction is sped up, this isn't all that time consuming.
// (unless its a REALLY big explosion)
if (throwForce <= 0)
return !tileBlocked;
list.Clear();
_entityLookup.FastEntitiesIntersecting(lookup, ref gridBox, entity => list.Add(entity));
foreach (var e in list)
{
// Here we only throw, no dealing damage. Containers n such might drop their entities after being destroyed, but
// they handle their own damage pass-through.
ProcessEntity(e, epicenter, processed, null, throwForce, id, false);
}
return !tileBlocked;
}
/// <summary>
/// Same as <see cref="ExplodeTile"/>, but for SPAAAAAAACE.
/// </summary>
internal void ExplodeSpace(EntityLookupComponent lookup,
Matrix3 spaceMatrix,
Matrix3 invSpaceMatrix,
Vector2i tile,
float intensity,
float throwForce,
DamageSpecifier damage,
MapCoordinates epicenter,
HashSet<EntityUid> processed,
string id)
{
var gridBox = new Box2(tile * DefaultTileSize, (DefaultTileSize, DefaultTileSize));
var worldBox = spaceMatrix.TransformBox(gridBox);
List<EntityUid> list = new();
EntityUidQueryCallback callback = uid =>
{
if (gridBox.Contains(invSpaceMatrix.Transform(Transform(uid).WorldPosition)))
list.Add(uid);
};
_entityLookup.FastEntitiesIntersecting(lookup, ref worldBox, callback);
foreach (var entity in list)
{
ProcessEntity(entity, epicenter, processed, damage, throwForce, id, false);
}
if (throwForce <= 0)
return;
list.Clear();
_entityLookup.FastEntitiesIntersecting(lookup, ref worldBox, callback);
foreach (var entity in list)
{
ProcessEntity(entity, epicenter, processed, null, throwForce, id, false);
}
}
/// <summary>
/// This function actually applies the explosion affects to an entity.
/// </summary>
private void ProcessEntity(EntityUid uid, MapCoordinates epicenter, HashSet<EntityUid> processed, DamageSpecifier? damage, float throwForce, string id, bool anchored)
{
// check whether this is a valid target, and whether we have already damaged this entity (can happen with
// explosion-throwing).
if (!anchored && _containerSystem.IsEntityInContainer(uid) || !processed.Add(uid))
return;
// damage
if (damage != null)
{
var ev = new GetExplosionResistanceEvent(id);
RaiseLocalEvent(uid, ev, false);
var coeff = Math.Clamp(0, 1 - ev.Resistance, 1);
if (!MathHelper.CloseTo(0, coeff))
_damageableSystem.TryChangeDamage(uid, damage * coeff, ignoreResistances: true);
}
// throw
if (!anchored
&& throwForce > 0
&& !EntityManager.IsQueuedForDeletion(uid)
&& HasComp<ExplosionLaunchedComponent>(uid)
&& TryComp(uid, out TransformComponent? transform))
{
uid.TryThrow(transform.WorldPosition - epicenter.Position, throwForce);
}
// TODO EXPLOSION puddle / flammable ignite?
// TODO EXPLOSION deaf/ear damage? other explosion effects?
}
/// <summary>
/// Tries to damage floor tiles. Not to be confused with the function that damages entities intersecting the
/// grid tile.
/// </summary>
public void DamageFloorTile(TileRef tileRef,
float intensity,
List<(Vector2i GridIndices, Tile Tile)> damagedTiles,
ExplosionPrototype type)
{
var tileDef = _tileDefinitionManager[tileRef.Tile.TypeId];
while (_robustRandom.Prob(type.TileBreakChance(intensity)))
{
intensity -= type.TileBreakRerollReduction;
if (tileDef is not ContentTileDefinition contentTileDef)
break;
// does this have a base-turf that we can break it down to?
if (contentTileDef.BaseTurfs.Count == 0)
break;
tileDef = _tileDefinitionManager[contentTileDef.BaseTurfs[^1]];
}
if (tileDef.TileId == tileRef.Tile.TypeId)
return;
damagedTiles.Add((tileRef.GridIndices, new Tile(tileDef.TileId)));
}
}
/// <summary>
/// This is a data class that stores information about the area affected by an explosion, for processing by <see
/// cref="ExplosionSystem"/>.
/// </summary>
/// <remarks>
/// This is basically the output of <see cref="ExplosionSystem.GetExplosionTiles()"/>, but wrapped in an enumerator
/// to iterate over the tiles, along with the ability to keep track of what entities have already been damaged by
/// this explosion.
/// </remarks>
sealed class Explosion
{
struct ExplosionData
{
public EntityLookupComponent Lookup;
public Dictionary<int, List<Vector2i>> TileLists;
public IMapGrid? MapGrid;
}
/// <summary>
/// Used to avoid applying explosion effects repeatedly to the same entity. Particularly important if the
/// explosion throws this entity, as then it will be moving while the explosion is happening.
/// </summary>
public readonly HashSet<EntityUid> ProcessedEntities = new();
/// <summary>
/// This integer tracks how much of this explosion has been processed.
/// </summary>
public int CurrentIteration { get; private set; } = 0;
public readonly ExplosionPrototype ExplosionType;
public readonly MapCoordinates Epicenter;
private readonly Matrix3 _spaceMatrix;
private readonly Matrix3 _invSpaceMatrix;
private readonly List<ExplosionData> _explosionData = new();
private readonly List<float> _tileSetIntensity;
public bool FinishedProcessing;
// shitty enumerator implementation
private DamageSpecifier _currentDamage = default!;
private EntityLookupComponent _currentLookup = default!;
private IMapGrid? _currentGrid;
private float _currentIntensity;
private float _currentThrowForce;
private List<Vector2i>.Enumerator _currentEnumerator;
private int _currentDataIndex;
private Dictionary<IMapGrid, List<(Vector2i, Tile)>> _tileUpdateDict = new();
public int Area;
private readonly ExplosionSystem _system;
public Explosion(ExplosionSystem system,
ExplosionPrototype explosionType,
SpaceExplosion? spaceData,
List<GridExplosion> gridData,
List<float> tileSetIntensity,
MapCoordinates epicenter,
Matrix3 spaceMatrix,
int area)
{
_system = system;
ExplosionType = explosionType;
_tileSetIntensity = tileSetIntensity;
Epicenter = epicenter;
Area = area;
var entityMan = IoCManager.Resolve<IEntityManager>();
var mapMan = IoCManager.Resolve<IMapManager>();
if (spaceData != null)
{
var mapUid = mapMan.GetMapEntityId(epicenter.MapId);
_explosionData.Add(new()
{
TileLists = spaceData.TileLists,
Lookup = entityMan.GetComponent<EntityLookupComponent>(mapUid),
MapGrid = null
});
_spaceMatrix = spaceMatrix;
_invSpaceMatrix = Matrix3.Invert(spaceMatrix);
}
foreach (var grid in gridData)
{
_explosionData.Add(new()
{
TileLists = grid.TileLists,
Lookup = entityMan.GetComponent<EntityLookupComponent>(grid.Grid.GridEntityId),
MapGrid = grid.Grid
});
}
TryGetNextTileEnumerator();
}
private bool TryGetNextTileEnumerator()
{
while (CurrentIteration < _tileSetIntensity.Count)
{
_currentIntensity = _tileSetIntensity[CurrentIteration];
_currentDamage = ExplosionType.DamagePerIntensity * _currentIntensity;
_currentThrowForce = Area > _system.ThrowLimit ? 0 : 10 * MathF.Sqrt(_currentIntensity);
// for each grid/space tile set
while (_currentDataIndex < _explosionData.Count)
{
// try get any tile hash-set corresponding to this intensity
var tileSets = _explosionData[_currentDataIndex].TileLists;
if (!tileSets.TryGetValue(CurrentIteration, out var tileList))
{
_currentDataIndex++;
continue;
}
_currentEnumerator = tileList.GetEnumerator();
_currentLookup = _explosionData[_currentDataIndex].Lookup;
_currentGrid = _explosionData[_currentDataIndex].MapGrid;
_currentDataIndex++;
return true;
}
// this explosion intensity has been fully processed, move to the next one
CurrentIteration++;
_currentDataIndex = 0;
}
// no more explosion data to process
FinishedProcessing = true;
return false;
}
private bool MoveNext()
{
if (FinishedProcessing)
return false;
while (!FinishedProcessing)
{
if (_currentEnumerator.MoveNext())
return true;
else
TryGetNextTileEnumerator();
}
return false;
}
public int Proccess(int processingTarget)
{
// In case the explosion terminated early last tick due to exceeding the allocated processing time, use this
// time to update the tiles.
SetTiles();
int processed;
for (processed = 0; processed < processingTarget; processed++)
{
if (processed % ExplosionSystem.TileCheckIteration == 0 &&
_system.Stopwatch.Elapsed.TotalMilliseconds > _system.MaxProcessingTime)
{
break;
}
if (_currentGrid != null &&
_currentGrid.TryGetTileRef(_currentEnumerator.Current, out var tileRef) &&
!tileRef.Tile.IsEmpty)
{
if (!_tileUpdateDict.TryGetValue(_currentGrid, out var tileUpdateList))
{
tileUpdateList = new();
_tileUpdateDict[_currentGrid] = tileUpdateList;
}
var canDamageFloor = _system.ExplodeTile(_currentLookup,
_currentGrid,
_currentEnumerator.Current,
_currentIntensity,
_currentThrowForce,
_currentDamage,
Epicenter,
ProcessedEntities,
ExplosionType.ID);
if (canDamageFloor)
_system.DamageFloorTile(tileRef, _currentIntensity, tileUpdateList, ExplosionType);
}
else
{
_system.ExplodeSpace(_currentLookup,
_spaceMatrix,
_invSpaceMatrix,
_currentEnumerator.Current,
_currentIntensity,
_currentThrowForce,
_currentDamage,
Epicenter,
ProcessedEntities,
ExplosionType.ID);
}
if (!MoveNext())
break;
}
SetTiles();
return processed;
}
private void SetTiles()
{
if (!_system.IncrementalTileBreaking && !FinishedProcessing)
return;
foreach (var (grid, list) in _tileUpdateDict)
{
if (list.Count > 0)
{
grid.SetTiles(list);
}
}
_tileUpdateDict.Clear();
}
}

View File

@@ -1,334 +0,0 @@
using System.Linq;
using Content.Shared.Administration;
using Content.Shared.Explosion;
using Robust.Shared.Map;
using Robust.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems;
// This partial part of the explosion system has all of the functions used to create the actual explosion map.
// I.e, to get the sets of tiles & intensity values that describe an explosion.
public sealed partial class ExplosionSystem : EntitySystem
{
/// <summary>
/// This is the main explosion generating function.
/// </summary>
/// <param name="epicenter">The center of the explosion</param>
/// <param name="typeID">The explosion type. this determines the explosion damage</param>
/// <param name="totalIntensity">The final sum of the tile intensities. This governs the overall size of the
/// explosion</param>
/// <param name="slope">How quickly does the intensity decrease when moving away from the epicenter.</param>
/// <param name="maxIntensity">The maximum intensity that the explosion can have at any given tile. This
/// effectively caps the damage that this explosion can do.</param>
/// <returns>A list of tile-sets and a list of intensity values which describe the explosion.</returns>
private (int, List<float>, SpaceExplosion?, Dictionary<GridId, GridExplosion>, Matrix3)? GetExplosionTiles(
MapCoordinates epicenter,
string typeID,
float totalIntensity,
float slope,
float maxIntensity)
{
if (totalIntensity <= 0 || slope <= 0)
return null;
Vector2i initialTile;
GridId? epicentreGrid = null;
var (localGrids, referenceGrid) = GetLocalGrids(epicenter, totalIntensity, slope, maxIntensity);
// get the epicenter tile indices
if (_mapManager.TryFindGridAt(epicenter, out var candidateGrid) &&
candidateGrid.TryGetTileRef(candidateGrid.WorldToTile(epicenter.Position), out var tileRef) &&
!tileRef.Tile.IsEmpty)
{
epicentreGrid = candidateGrid.Index;
initialTile = tileRef.GridIndices;
}
else if (referenceGrid != null)
{
// reference grid defines coordinate system that the explosion in space will use
initialTile = _mapManager.GetGrid(referenceGrid.Value).WorldToTile(epicenter.Position);
}
else
{
// this is a space-based explosion that (should) not touch any grids.
initialTile = new Vector2i(
(int) Math.Floor(epicenter.Position.X / DefaultTileSize),
(int) Math.Floor(epicenter.Position.Y / DefaultTileSize));
}
// Main data for the exploding tiles in space and on various grids
Dictionary<GridId, GridExplosion> gridData = new();
SpaceExplosion? spaceData = null;
// The intensity slope is how much the intensity drop over a one-tile distance. The actual algorithm step-size is half of thhat.
var stepSize = slope / 2;
// Hashsets used for when grid-based explosion propagate into space. Basically: used to move data between
// `gridData` and `spaceData` in-between neighbor finding iterations.
HashSet<Vector2i> spaceJump = new();
HashSet<Vector2i> previousSpaceJump;
// As above, but for space-based explosion propagating from space onto grids.
HashSet<GridId> encounteredGrids = new();
Dictionary<GridId, HashSet<Vector2i>>? previousGridJump;
// variables for transforming between grid and space-coordiantes
var spaceMatrix = Matrix3.Identity;
var spaceAngle = Angle.Zero;
if (referenceGrid != null)
{
var xform = Transform(_mapManager.GetGrid(referenceGrid.Value).GridEntityId);
spaceMatrix = xform.WorldMatrix;
spaceAngle = xform.WorldRotation;
}
// is the explosion starting on a grid?
if (epicentreGrid != null)
{
// set up the initial `gridData` instance
encounteredGrids.Add(epicentreGrid.Value);
if (!_airtightMap.TryGetValue(epicentreGrid.Value, out var airtightMap))
airtightMap = new();
var initialGridData = new GridExplosion(
_mapManager.GetGrid(epicentreGrid.Value),
airtightMap,
maxIntensity,
stepSize,
typeID,
_gridEdges[epicentreGrid.Value],
referenceGrid,
spaceMatrix,
spaceAngle);
gridData[epicentreGrid.Value] = initialGridData;
initialGridData.InitTile(initialTile);
}
else
{
// set up the space explosion data
spaceData = new SpaceExplosion(this, epicenter.MapId, referenceGrid, localGrids);
spaceData.InitTile(initialTile);
}
// Is this even a multi-tile explosion?
if (totalIntensity < stepSize)
// Bit anticlimactic. All that set up for nothing....
return (1, new List<float> { totalIntensity }, spaceData, gridData, spaceMatrix);
// These variables keep track of the total intensity we have distributed
List<int> tilesInIteration = new() { 1 };
List<float> iterationIntensity = new() {stepSize};
var totalTiles = 0;
var remainingIntensity = totalIntensity - stepSize;
var iteration = 1;
var maxIntensityIndex = 0;
// If an explosion is trapped in an indestructible room, we can end the neighbor finding steps early.
// These variables are used to check if we can abort early.
float previousIntensity;
var intensityUnchangedLastLoop = false;
// Main flood-fill / neighbor-finding loop
while (remainingIntensity > 0 && iteration <= MaxIterations && totalTiles < MaxArea)
{
previousIntensity = remainingIntensity;
// First, we increase the intensity of the tiles that were already discovered in previous iterations.
for (var i = maxIntensityIndex; i < iteration; i++)
{
var intensityIncrease = MathF.Min(stepSize, maxIntensity - iterationIntensity[i]);
if (tilesInIteration[i] * intensityIncrease >= remainingIntensity)
{
// there is not enough intensity left to distribute. add a fractional amount and break.
iterationIntensity[i] += (float) remainingIntensity / tilesInIteration[i];
remainingIntensity = 0;
break;
}
iterationIntensity[i] += intensityIncrease;
remainingIntensity -= tilesInIteration[i] * intensityIncrease;
// Has this tile-set has reached max intensity? If so, stop iterating over it in future
if (intensityIncrease < stepSize)
maxIntensityIndex++;
}
if (remainingIntensity <= 0) break;
// Next, we will add a new iteration of tiles
// In order to treat "cost" of moving off a grid on the same level as moving onto a grid, both space -> grid and grid -> space have to be delayed by one iteration.
previousSpaceJump = spaceJump;
previousGridJump = spaceData?.GridJump;
spaceJump = new();
var newTileCount = 0;
if (previousGridJump != null)
encounteredGrids.UnionWith(previousGridJump.Keys);
foreach (var grid in encounteredGrids)
{
// is this a new grid, for which we must create a new explosion data set
if (!gridData.TryGetValue(grid, out var data))
{
if (!_airtightMap.TryGetValue(grid, out var airtightMap))
airtightMap = new();
data = new GridExplosion(
_mapManager.GetGrid(grid),
airtightMap,
maxIntensity,
stepSize,
typeID,
_gridEdges[grid],
referenceGrid,
spaceMatrix,
spaceAngle);
gridData[grid] = data;
}
// get the new neighbours, and populate gridToSpaceTiles in the process.
newTileCount += data.AddNewTiles(iteration, previousGridJump?.GetValueOrDefault(grid));
spaceJump.UnionWith(data.SpaceJump);
}
// if space-data is null, but some grid-based explosion reached space, we need to initialize it.
if (spaceData == null && previousSpaceJump.Count != 0)
spaceData = new SpaceExplosion(this, epicenter.MapId, referenceGrid, localGrids);
// If the explosion has reached space, do that neighbors finding step as well.
if (spaceData != null)
newTileCount += spaceData.AddNewTiles(iteration, previousSpaceJump);
// Does adding these tiles bring us above the total target intensity?
tilesInIteration.Add(newTileCount);
if (newTileCount * stepSize >= remainingIntensity)
{
iterationIntensity.Add((float) remainingIntensity / newTileCount);
break;
}
// add the new tiles and decrement available intensity
remainingIntensity -= newTileCount * stepSize;
iterationIntensity.Add(stepSize);
totalTiles += newTileCount;
// It is possible that the explosion has some max intensity and is stuck in a container whose walls it
// cannot break. if the remaining intensity remains unchanged TWO loops in a row, we know that this is the
// case.
if (intensityUnchangedLastLoop && remainingIntensity == previousIntensity)
break;
intensityUnchangedLastLoop = remainingIntensity == previousIntensity;
iteration += 1;
}
if (totalTiles >= MaxArea)
Logger.Info("Whooooo! MAXCAP!");
// Neighbor finding is done. Perform final clean up and return.
foreach (var grid in gridData.Values)
{
grid.CleanUp();
}
spaceData?.CleanUp();
return (totalTiles, iterationIntensity, spaceData, gridData, spaceMatrix);
}
/// <summary>
/// Look for grids in an area and returns them. Also selects a special grid that will be used to determine the
/// orientation of an explosion in space.
/// </summary>
/// <remarks>
/// Note that even though an explosion may start ON a grid, the explosion in space may still be orientated to
/// match a separate grid. This is done so that if you have something like a tiny suicide-bomb shuttle exploding
/// near a large station, the explosion will still orient to match the station, not the tiny shuttle.
/// </remarks>
public (List<GridId>, GridId?) GetLocalGrids(MapCoordinates epicenter, float totalIntensity, float slope, float maxIntensity)
{
// Get the explosion radius (approx radius if it were in open-space). Note that if the explosion is confined in
// some directions but not in others, the actual explosion may reach further than this distance from the
// epicenter. Conversely, it might go nowhere near as far.
var radius = 0.5f + IntensityToRadius(totalIntensity, slope, maxIntensity);
// to avoid a silly lookup for silly input numbers, cap the radius to half of the theoretical maximum (lookup area gets doubled later on).
radius = Math.Min(radius, MaxIterations / 4);
GridId? referenceGrid = null;
float mass = 0;
// First attempt to find a grid that is relatively close to the explosion's center. Instead of looking in a
// diameter x diameter sized box, use a smaller box with radius sized sides:
var box = Box2.CenteredAround(epicenter.Position, (radius, radius));
foreach (var grid in _mapManager.FindGridsIntersecting(epicenter.MapId, box))
{
if (TryComp(grid.GridEntityId, out PhysicsComponent? physics) && physics.Mass > mass)
{
mass = physics.Mass;
referenceGrid = grid.Index;
}
}
// Next, we use a much larger lookup to determine all grids relevant to the explosion. This is used to determine
// what grids should be includes during the grid-edge transformation steps. This means that if a grid is not in
// this set, the explosion can never propagate from space onto this grid.
// As mentioned before, the `diameter` is only indicative, as an explosion that is obstructed (e.g., in a
// tunnel) may travel further away from the epicenter. But this should be very rare for space-traversing
// explosions. So instead of using the largest possible distance that an explosion could theoretically travel
// and using that for the grid look-up, we will just arbitrarily fudge the lookup size to be twice the diameter.
box = box.Scale(4); // box with width and height of 4*radius.
var mapGrids = _mapManager.FindGridsIntersecting(epicenter.MapId, box).ToList();
var grids = mapGrids.Select(x => x.Index).ToList();
if (referenceGrid != null)
return (grids, referenceGrid);
// We still don't have are reference grid. So lets also look in the enlarged region
foreach (var grid in mapGrids)
{
if (TryComp(grid.GridEntityId, out PhysicsComponent? physics) && physics.Mass > mass)
{
mass = physics.Mass;
referenceGrid = grid.Index;
}
}
return (grids, referenceGrid);
}
public ExplosionEvent? GenerateExplosionPreview(SpawnExplosionEuiMsg.PreviewRequest request)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
var results = GetExplosionTiles(
request.Epicenter,
request.TypeId,
request.TotalIntensity,
request.IntensitySlope,
request.MaxIntensity);
if (results == null)
return null;
var (area, iterationIntensity, spaceData, gridData, spaceMatrix) = results.Value;
Logger.Info($"Generated explosion preview with {area} tiles in {stopwatch.Elapsed.TotalMilliseconds}ms");
// the explosion event that **would** be sent to all clients, if it were a real explosion.
return GetExplosionEvent(request.Epicenter, request.TypeId, spaceMatrix, spaceData, gridData.Values, iterationIntensity);
}
}

View File

@@ -1,274 +1,392 @@
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Server.Atmos.Components; using Content.Server.Administration.Logs;
using Content.Server.Explosion.Components; using Content.Server.Explosion.Components;
using Content.Server.NodeContainer.EntitySystems; using Content.Shared.Acts;
using Content.Shared.Camera; using Content.Shared.Camera;
using Content.Shared.Damage; using Content.Shared.Database;
using Content.Shared.Explosion; using Content.Shared.Interaction;
using Robust.Server.Containers; using Content.Shared.Interaction.Helpers;
using Robust.Server.Player; using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Sound;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Configuration; using Robust.Shared.Containers;
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;
namespace Content.Server.Explosion.EntitySystems; namespace Content.Server.Explosion.EntitySystems
public sealed partial class ExplosionSystem : EntitySystem
{ {
[Dependency] private readonly IMapManager _mapManager = default!; public sealed class ExplosionSystem : EntitySystem
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly NodeGroupSystem _nodeGroupSystem = default!;
[Dependency] private readonly CameraRecoilSystem _recoilSystem = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
/// <summary>
/// "Tile-size" for space when there are no nearby grids to use as a reference.
/// </summary>
public const ushort DefaultTileSize = 1;
private AudioParams _audioParams = AudioParams.Default.WithVolume(-3f);
public override void Initialize()
{ {
base.Initialize(); [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
// handled in ExplosionSystemGridMap.cs /// <summary>
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved); /// Distance used for camera shake when distance from explosion is (0.0, 0.0).
SubscribeLocalEvent<GridStartupEvent>(OnGridStartup); /// Avoids getting NaN values down the line from doing math on (0.0, 0.0).
SubscribeLocalEvent<ExplosionResistanceComponent, GetExplosionResistanceEvent>(OnGetResistance); /// </summary>
_mapManager.TileChanged += OnTileChanged; private static readonly Vector2 EpicenterDistance = (0.1f, 0.1f);
// handled in ExplosionSystemAirtight.cs /// <summary>
SubscribeLocalEvent<AirtightComponent, DamageChangedEvent>(OnAirtightDamaged); /// Chance of a tile breaking if the severity is Light and Heavy
SubscribeCvars(); /// </summary>
} private const float LightBreakChance = 0.3f;
private const float HeavyBreakChance = 0.8f;
public override void Shutdown() // TODO move this to the component
{ private static readonly SoundSpecifier ExplosionSound = new SoundCollectionSpecifier("explosion");
base.Shutdown();
_mapManager.TileChanged -= OnTileChanged;
UnsubscribeCvars();
}
private void OnGetResistance(EntityUid uid, ExplosionResistanceComponent component, GetExplosionResistanceEvent args) [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
{ [Dependency] private readonly IGameTiming _timing = default!;
args.Resistance += component.GlobalResistance; [Dependency] private readonly IMapManager _maps = default!;
if (component.Resistances.TryGetValue(args.ExplotionPrototype, out var resistance)) [Dependency] private readonly IRobustRandom _random = default!;
args.Resistance += resistance; [Dependency] private readonly ITileDefinitionManager _tiles = default!;
}
/// <summary> [Dependency] private readonly ActSystem _acts = default!;
/// Given an entity with an explosive component, spawn the appropriate explosion. [Dependency] private readonly EffectSystem _effects = default!;
/// </summary> [Dependency] private readonly TriggerSystem _triggers = default!;
/// <remarks> [Dependency] private readonly AdminLogSystem _logSystem = default!;
/// Also accepts radius or intensity arguments. This is useful for explosives where the intensity is not [Dependency] private readonly CameraRecoilSystem _cameraRecoil = default!;
/// specified in the yaml / by the component, but determined dynamically (e.g., by the quantity of a [Dependency] private readonly TagSystem _tags = default!;
/// solution in a reaction).
/// </remarks>
public void TriggerExplosive(EntityUid uid, ExplosiveComponent? explosive = null, bool delete = true, float? totalIntensity = null, float? radius = null)
{
// log missing: false, because some entities (e.g. liquid tanks) attempt to trigger explosions when damaged,
// but may not actually be explosive.
if (!Resolve(uid, ref explosive, logMissing: false))
return;
// No reusable explosions here. private bool IgnoreExplosivePassable(EntityUid e)
if (explosive.Exploded)
return;
explosive.Exploded = true;
// Override the explosion intensity if optional arguments were provided.
if (radius != null)
totalIntensity ??= RadiusToIntensity((float) radius, explosive.IntensitySlope, explosive.MaxIntensity);
totalIntensity ??= explosive.TotalIntensity;
QueueExplosion(uid,
explosive.ExplosionType,
(float) totalIntensity,
explosive.IntensitySlope,
explosive.MaxIntensity);
if (delete)
EntityManager.QueueDeleteEntity(uid);
}
/// <summary>
/// Find the strength needed to generate an explosion of a given radius. More useful for radii larger then 4, when the explosion becomes less "blocky".
/// </summary>
/// <remarks>
/// This assumes the explosion is in a vacuum / unobstructed. Given that explosions are not perfectly
/// circular, here radius actually means the sqrt(Area/pi), where the area is the total number of tiles
/// covered by the explosion. Until you get to radius 30+, this is functionally equivalent to the
/// actual radius.
/// </remarks>
public float RadiusToIntensity(float radius, float slope, float maxIntensity = 0)
{
// If you consider the intensity at each tile in an explosion to be a height. Then a circular explosion is
// shaped like a cone. So total intensity is like the volume of a cone with height = slope * radius. Of
// course, as the explosions are not perfectly circular, this formula isn't perfect, but the formula works
// reasonably well.
// This should actually use the formula for the volume of a distorted octagonal frustum. But this is good
// enough.
var coneVolume = slope * MathF.PI / 3 * MathF.Pow(radius, 3);
if (maxIntensity <= 0 || slope * radius < maxIntensity)
return coneVolume;
// This explosion is limited by the maxIntensity.
// Instead of a cone, we have a conical frustum.
// Subtract the volume of the missing cone segment, with height:
var h = slope * radius - maxIntensity;
return coneVolume - h * MathF.PI / 3 * MathF.Pow(h / slope, 2);
}
/// <summary>
/// Inverse formula for <see cref="RadiusToIntensity"/>
/// </summary>
public float IntensityToRadius(float totalIntensity, float slope, float maxIntensity)
{
// max radius to avoid being capped by max-intensity
var r0 = maxIntensity / slope;
// volume at r0
var v0 = RadiusToIntensity(r0, slope);
if (totalIntensity <= v0)
{ {
// maxIntensity is a non-issue, can use simple inverse formula return _tags.HasTag(e, "ExplosivePassable");
return MathF.Cbrt(3 * totalIntensity / (slope * MathF.PI));
} }
return r0 * (MathF.Sqrt(12 * totalIntensity/ v0 - 3) / 6 + 0.5f); private ExplosionSeverity CalculateSeverity(float distance, float devastationRange, float heavyRange)
}
/// <summary>
/// Queue an explosions, centered on some entity.
/// </summary>
public void QueueExplosion(EntityUid uid,
string typeId,
float intensity,
float slope,
float maxTileIntensity)
{
QueueExplosion(Transform(uid).MapPosition, typeId, intensity, slope, maxTileIntensity);
}
/// <summary>
/// Queue an explosion, with a specified epicenter and set of starting tiles.
/// </summary>
public void QueueExplosion(MapCoordinates epicenter,
string typeId,
float totalIntensity,
float slope,
float maxTileIntensity)
{
if (totalIntensity <= 0 || slope <= 0)
return;
if (!_prototypeManager.TryIndex<ExplosionPrototype>(typeId, out var type))
{ {
Logger.Error($"Attempted to spawn unknown explosion prototype: {type}"); if (distance < devastationRange)
return; {
return ExplosionSeverity.Destruction;
}
else if (distance < heavyRange)
{
return ExplosionSeverity.Heavy;
}
else
{
return ExplosionSeverity.Light;
}
} }
_explosionQueue.Enqueue(() => SpawnExplosion(epicenter, type, totalIntensity, private void CameraShakeInRange(EntityCoordinates epicenter, float maxRange)
slope, maxTileIntensity));
}
/// <summary>
/// This function actually spawns the explosion. It returns an <see cref="Explosion"/> instance with
/// information about the affected tiles for the explosion system to process. It will also trigger the
/// camera shake and sound effect.
/// </summary>
private Explosion? SpawnExplosion(MapCoordinates epicenter,
ExplosionPrototype type,
float totalIntensity,
float slope,
float maxTileIntensity)
{
var results = GetExplosionTiles(epicenter, type.ID, totalIntensity, slope, maxTileIntensity);
if (results == null)
return null;
var (area, iterationIntensity, spaceData, gridData, spaceMatrix) = results.Value;
RaiseNetworkEvent(GetExplosionEvent(epicenter, type.ID, spaceMatrix, spaceData, gridData.Values, iterationIntensity));
// camera shake
CameraShake(iterationIntensity.Count * 2.5f, epicenter, totalIntensity);
//For whatever bloody reason, sound system requires ENTITY coordinates.
var mapEntityCoords = EntityCoordinates.FromMap(EntityManager, _mapManager.GetMapEntityId(epicenter.MapId), epicenter);
// play sound.
var audioRange = iterationIntensity.Count * 5;
var filter = Filter.Pvs(epicenter).AddInRange(epicenter, audioRange);
SoundSystem.Play(filter, type.Sound.GetSound(), mapEntityCoords, _audioParams);
return new Explosion(this,
type,
spaceData,
gridData.Values.ToList(),
iterationIntensity,
epicenter,
spaceMatrix,
area
);
}
/// <summary>
/// Constructor for the shared <see cref="ExplosionEvent"/> using the server-exclusive explosion classes.
/// </summary>
internal ExplosionEvent GetExplosionEvent(MapCoordinates epicenter, string id, Matrix3 spaceMatrix, SpaceExplosion? spaceData, IEnumerable<GridExplosion> gridData, List<float> iterationIntensity)
{
var spaceTiles = spaceData?.TileLists;
Dictionary<GridId, Dictionary<int, List<Vector2i>>> tileLists = new();
foreach (var grid in gridData)
{ {
tileLists.Add(grid.Grid.Index, grid.TileLists); var players = Filter.Empty()
.AddInRange(epicenter.ToMap(EntityManager), MathF.Ceiling(maxRange))
.Recipients;
foreach (var player in players)
{
if (player.AttachedEntity is not {Valid: true} playerEntity ||
!EntityManager.HasComponent<CameraRecoilComponent>(playerEntity))
{
continue;
}
var playerPos = EntityManager.GetComponent<TransformComponent>(playerEntity).WorldPosition;
var delta = epicenter.ToMapPos(EntityManager) - playerPos;
//Change if zero. Will result in a NaN later breaking camera shake if not changed
if (delta.EqualsApprox((0.0f, 0.0f)))
delta = EpicenterDistance;
var distance = delta.LengthSquared;
var effect = 10 * (1 / (1 + distance));
if (effect > 0.01f)
{
var kick = -delta.Normalized * effect;
_cameraRecoil.KickCamera(player.AttachedEntity.Value, kick);
}
}
} }
return new ExplosionEvent(_explosionCounter, epicenter, id, iterationIntensity, spaceTiles, tileLists, spaceMatrix); /// <summary>
} /// Damage entities inside the range. The damage depends on a discrete
/// damage bracket [light, heavy, devastation] and the distance from the epicenter
private void CameraShake(float range, MapCoordinates epicenter, float totalIntensity) /// </summary>
{ /// <returns>
var players = Filter.Empty(); /// A dictionary of coordinates relative to the parents of every grid of entities that survived the explosion,
players.AddInRange(epicenter, range, _playerManager, EntityManager); /// have an airtight component and are currently blocking air. Like a wall.
/// </returns>
foreach (var player in players.Recipients) private void DamageEntitiesInRange(
EntityCoordinates epicenter,
Box2 boundingBox,
float devastationRange,
float heavyRange,
float maxRange,
MapId mapId)
{ {
if (player.AttachedEntity is not EntityUid uid) var entitiesInRange = _entityLookup.GetEntitiesInRange(mapId, boundingBox, 0).ToList();
continue;
var playerPos = Transform(player.AttachedEntity!.Value).WorldPosition; var impassableEntities = new List<(EntityUid, float)>();
var delta = epicenter.Position - playerPos; var nonImpassableEntities = new List<(EntityUid, float)>();
// TODO: Given this seems to rely on physics it should just query directly like everything else.
if (delta.EqualsApprox(Vector2.Zero)) // The entities are paired with their distance to the epicenter
delta = new(0.01f, 0); // and splitted into two lists based on if they are Impassable or not
foreach (var entity in entitiesInRange)
{
if (Deleted(entity) || entity.IsInContainer())
{
continue;
}
var distance = delta.Length; if (!EntityManager.GetComponent<TransformComponent>(entity).Coordinates.TryDistance(EntityManager, epicenter, out var distance) ||
var effect = 5 * MathF.Pow(totalIntensity, 0.5f) * (1 - distance / range); distance > maxRange)
if (effect > 0.01f) {
_recoilSystem.KickCamera(uid, -delta.Normalized * effect); continue;
}
if (!EntityManager.TryGetComponent(entity, out FixturesComponent? fixturesComp) || fixturesComp.Fixtures.Count < 1)
{
continue;
}
if (!EntityManager.TryGetComponent(entity, out PhysicsComponent? body))
{
continue;
}
if ((body.CollisionLayer & (int) CollisionGroup.Impassable) != 0)
{
impassableEntities.Add((entity, distance));
}
else
{
nonImpassableEntities.Add((entity, distance));
}
}
// The Impassable entities are sorted in descending order
// Entities closer to the epicenter are first
impassableEntities.Sort((x, y) => x.Item2.CompareTo(y.Item2));
// Impassable entities are handled first. If they are damaged enough, they are destroyed and they may
// be able to spawn a new entity. I.e Wall -> Girder.
// Girder has a tag ExplosivePassable, and the predicate make it so the entities with this tag are ignored
var epicenterMapPos = epicenter.ToMap(EntityManager);
foreach (var (entity, distance) in impassableEntities)
{
if (!_interactionSystem.InRangeUnobstructed(epicenterMapPos, entity, maxRange, predicate: IgnoreExplosivePassable))
{
continue;
}
_acts.HandleExplosion(epicenter, entity, CalculateSeverity(distance, devastationRange, heavyRange));
}
// Impassable entities were handled first so NonImpassable entities have a bigger chance to get hit. As now
// there are probably more ExplosivePassable entities around
foreach (var (entity, distance) in nonImpassableEntities)
{
if (!_interactionSystem.InRangeUnobstructed(epicenterMapPos, entity, maxRange, predicate: IgnoreExplosivePassable))
{
continue;
}
_acts.HandleExplosion(epicenter, entity, CalculateSeverity(distance, devastationRange, heavyRange));
}
}
/// <summary>
/// Damage tiles inside the range. The type of tile can change depending on a discrete
/// damage bracket [light, heavy, devastation], the distance from the epicenter and
/// a probability bracket [<see cref="LightBreakChance"/>, <see cref="HeavyBreakChance"/>, 1.0].
/// </summary>
///
private void DamageTilesInRange(EntityCoordinates epicenter,
GridId gridId,
Box2 boundingBox,
float devastationRange,
float heaveyRange,
float maxRange)
{
if (!_maps.TryGetGrid(gridId, out var mapGrid))
{
return;
}
if (!EntityManager.EntityExists(mapGrid.GridEntityId))
{
return;
}
var tilesInGridAndCircle = mapGrid.GetTilesIntersecting(boundingBox);
var epicenterMapPos = epicenter.ToMap(EntityManager);
foreach (var tile in tilesInGridAndCircle)
{
var tileLoc = mapGrid.GridTileToLocal(tile.GridIndices);
if (!tileLoc.TryDistance(EntityManager, epicenter, out var distance) ||
distance > maxRange)
{
continue;
}
if (tile.IsBlockedTurf(false))
{
continue;
}
if (!_interactionSystem.InRangeUnobstructed(tileLoc.ToMap(EntityManager), epicenterMapPos, maxRange, predicate: IgnoreExplosivePassable))
{
continue;
}
var tileDef = (ContentTileDefinition) _tiles[tile.Tile.TypeId];
var baseTurfs = tileDef.BaseTurfs;
if (baseTurfs.Count == 0)
{
continue;
}
var zeroTile = new Tile(_tiles[baseTurfs[0]].TileId);
var previousTile = new Tile(_tiles[baseTurfs[^1]].TileId);
var severity = CalculateSeverity(distance, devastationRange, heaveyRange);
switch (severity)
{
case ExplosionSeverity.Light:
if (!previousTile.IsEmpty && _random.Prob(LightBreakChance))
{
mapGrid.SetTile(tileLoc, previousTile);
}
break;
case ExplosionSeverity.Heavy:
if (!previousTile.IsEmpty && _random.Prob(HeavyBreakChance))
{
mapGrid.SetTile(tileLoc, previousTile);
}
break;
case ExplosionSeverity.Destruction:
mapGrid.SetTile(tileLoc, zeroTile);
break;
}
}
}
private void FlashInRange(EntityCoordinates epicenter, float flashRange)
{
if (flashRange > 0)
{
var time = _timing.CurTime;
var message = new EffectSystemMessage
{
EffectSprite = "Effects/explosion.rsi",
RsiState = "explosionfast",
Born = time,
DeathTime = time + TimeSpan.FromSeconds(5),
Size = new Vector2(flashRange / 2, flashRange / 2),
Coordinates = epicenter,
Rotation = 0f,
ColorDelta = new Vector4(0, 0, 0, -1500f),
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), 0.5f),
Shaded = false
};
_effects.CreateParticle(message);
}
}
public void SpawnExplosion(
EntityUid entity,
int devastationRange = 0,
int heavyImpactRange = 0,
int lightImpactRange = 0,
int flashRange = 0,
EntityUid? user = null,
ExplosiveComponent? explosive = null,
TransformComponent? transform = null)
{
if (!Resolve(entity, ref transform))
{
return;
}
Resolve(entity, ref explosive, false);
if (explosive is { Exploding: false })
{
_triggers.Explode(entity, explosive, user);
}
else
{
while (EntityManager.EntityExists(entity) && entity.TryGetContainer(out var container))
{
entity = container.Owner;
}
if (!EntityManager.TryGetComponent(entity, out transform))
{
return;
}
var epicenter = transform.Coordinates;
SpawnExplosion(epicenter, devastationRange, heavyImpactRange, lightImpactRange, flashRange, entity, user);
}
}
public void SpawnExplosion(
EntityCoordinates epicenter,
int devastationRange = 0,
int heavyImpactRange = 0,
int lightImpactRange = 0,
int flashRange = 0,
EntityUid? entity = null,
EntityUid? user = null)
{
var mapId = epicenter.GetMapId(EntityManager);
if (mapId == MapId.Nullspace)
{
return;
}
// logging
var range = $"{devastationRange}/{heavyImpactRange}/{lightImpactRange}/{flashRange}";
if (entity == null || !entity.Value.IsValid())
{
_logSystem.Add(LogType.Explosion, LogImpact.High, $"Explosion spawned at {epicenter:coordinates} with range {range}");
}
else if (user == null || !user.Value.IsValid())
{
_logSystem.Add(LogType.Explosion, LogImpact.High,
$"{ToPrettyString(entity.Value):entity} exploded at {epicenter:coordinates} with range {range}");
}
else
{
_logSystem.Add(LogType.Explosion, LogImpact.High,
$"{ToPrettyString(user.Value):user} caused {ToPrettyString(entity.Value):entity} to explode at {epicenter:coordinates} with range {range}");
}
var maxRange = MathHelper.Max(devastationRange, heavyImpactRange, lightImpactRange, 0);
var epicenterMapPos = epicenter.ToMapPos(EntityManager);
var boundingBox = new Box2(epicenterMapPos - new Vector2(maxRange, maxRange),
epicenterMapPos + new Vector2(maxRange, maxRange));
SoundSystem.Play(Filter.Broadcast(), ExplosionSound.GetSound(), epicenter);
DamageEntitiesInRange(epicenter, boundingBox, devastationRange, heavyImpactRange, maxRange, mapId);
var mapGridsNear = _maps.FindGridsIntersecting(mapId, boundingBox);
foreach (var gridId in mapGridsNear)
{
DamageTilesInRange(epicenter, gridId.Index, boundingBox, devastationRange, heavyImpactRange, maxRange);
}
CameraShakeInRange(epicenter, maxRange);
FlashInRange(epicenter, flashRange);
} }
} }
} }

View File

@@ -1,44 +0,0 @@
using Content.Shared.CCVar;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class ExplosionSystem : EntitySystem
{
public int MaxIterations { get; private set; }
public int MaxArea { get; private set; }
public float MaxProcessingTime { get; private set; }
public int TilesPerTick { get; private set; }
public int ThrowLimit { get; private set; }
public bool SleepNodeSys { get; private set; }
public bool IncrementalTileBreaking { get; private set; }
private void SubscribeCvars()
{
_cfg.OnValueChanged(CCVars.ExplosionTilesPerTick, SetTilesPerTick, true);
_cfg.OnValueChanged(CCVars.ExplosionThrowLimit, SetThrowLimit, true);
_cfg.OnValueChanged(CCVars.ExplosionSleepNodeSys, SetSleepNodeSys, true);
_cfg.OnValueChanged(CCVars.ExplosionMaxArea, SetMaxArea, true);
_cfg.OnValueChanged(CCVars.ExplosionMaxIterations, SetMaxIterations, true);
_cfg.OnValueChanged(CCVars.ExplosionMaxProcessingTime, SetMaxProcessingTime, true);
_cfg.OnValueChanged(CCVars.ExplosionIncrementalTileBreaking, SetIncrementalTileBreaking, true);
}
private void UnsubscribeCvars()
{
_cfg.UnsubValueChanged(CCVars.ExplosionTilesPerTick, SetTilesPerTick);
_cfg.UnsubValueChanged(CCVars.ExplosionThrowLimit, SetThrowLimit);
_cfg.UnsubValueChanged(CCVars.ExplosionSleepNodeSys, SetSleepNodeSys);
_cfg.UnsubValueChanged(CCVars.ExplosionMaxArea, SetMaxArea);
_cfg.UnsubValueChanged(CCVars.ExplosionMaxIterations, SetMaxIterations);
_cfg.UnsubValueChanged(CCVars.ExplosionMaxProcessingTime, SetMaxProcessingTime);
_cfg.UnsubValueChanged(CCVars.ExplosionIncrementalTileBreaking, SetIncrementalTileBreaking);
}
private void SetTilesPerTick(int value) => TilesPerTick = value;
private void SetThrowLimit(int value) => ThrowLimit = value;
private void SetSleepNodeSys(bool value) => SleepNodeSys = value;
private void SetMaxArea(int value) => MaxArea = value;
private void SetMaxIterations(int value) => MaxIterations = value;
private void SetMaxProcessingTime(float value) => MaxProcessingTime = value;
private void SetIncrementalTileBreaking(bool value) => IncrementalTileBreaking = value;
}

View File

@@ -1,311 +0,0 @@
using Content.Shared.Atmos;
using Robust.Shared.Map;
namespace Content.Server.Explosion.EntitySystems;
internal sealed class GridExplosion : TileExplosion
{
public IMapGrid Grid;
private bool _needToTransform = false;
private Matrix3 _matrix = Matrix3.Identity;
private Vector2 _offset;
private HashSet<Vector2i> _processedSpaceTiles = new();
// Tiles which neighbor an exploding tile, but have not yet had the explosion spread to them due to an
// airtight entity on the exploding tile that prevents the explosion from spreading in that direction. These
// will be added as a neighbor after some delay, once the explosion on that tile is sufficiently strong to
// destroy the airtight entity.
private Dictionary<int, List<(Vector2i, AtmosDirection)>> _delayedNeighbors = new();
private Dictionary<Vector2i, TileData> _airtightMap;
private float _maxIntensity;
private float _intensityStepSize;
private string _typeID;
/// <summary>
/// Tiles on this grid that are not actually on this grid.... uhh ... yeah.... look its faster than checking
/// atmos directions every iteration.
/// </summary>
private HashSet<Vector2i> _spaceTiles = new();
public HashSet<Vector2i> SpaceJump = new();
private Dictionary<Vector2i, AtmosDirection> _edgeTiles;
public GridExplosion(
IMapGrid grid,
Dictionary<Vector2i, TileData> airtightMap,
float maxIntensity,
float intensityStepSize,
string typeID,
Dictionary<Vector2i, AtmosDirection> edgeTiles,
GridId? referenceGrid,
Matrix3 spaceMatrix,
Angle spaceAngle)
{
Grid = grid;
_airtightMap = airtightMap;
_maxIntensity = maxIntensity;
_intensityStepSize = intensityStepSize;
_typeID = typeID;
_edgeTiles = edgeTiles;
// initialise SpaceTiles
foreach (var (tile, dir) in _edgeTiles)
{
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
if (dir.IsFlagSet(direction))
_spaceTiles.Add(tile.Offset(direction));
}
}
// TODO EXPLOSIONS fix this shit.
foreach (var tile in _edgeTiles.Keys)
{
foreach (var diagTile in ExplosionSystem.GetDiagonalNeighbors(tile))
{
if (_spaceTiles.Contains(diagTile))
continue;
if (!Grid.TryGetTileRef(diagTile, out var tileRef) || tileRef.Tile.IsEmpty)
_spaceTiles.Add(diagTile);
}
}
if (referenceGrid == Grid.Index)
return;
_needToTransform = true;
var transform = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Grid.GridEntityId);
var size = (float) Grid.TileSize;
_matrix.R0C2 = size / 2;
_matrix.R1C2 = size / 2;
_matrix *= transform.WorldMatrix * Matrix3.Invert(spaceMatrix);
var relativeAngle = transform.WorldRotation - spaceAngle;
_offset = relativeAngle.RotateVec((size / 4, size / 4));
}
public int AddNewTiles(int iteration, HashSet<Vector2i>? gridJump)
{
SpaceJump = new();
NewTiles = new();
NewBlockedTiles = new();
// Mark tiles as entered if any were just freed due to airtight/explosion blockers being destroyed.
if (FreedTileLists.TryGetValue(iteration, out var freed))
{
freed.ExceptWith(EnteredBlockedTiles);
EnteredBlockedTiles.UnionWith(freed);
NewFreedTiles = freed;
}
else
{
NewFreedTiles = new();
FreedTileLists[iteration] = NewFreedTiles;
}
// Add adjacent tiles
if (TileLists.TryGetValue(iteration - 2, out var adjacent))
AddNewAdjacentTiles(iteration, adjacent, false);
if (FreedTileLists.TryGetValue(iteration - 2, out var delayedAdjacent))
AddNewAdjacentTiles(iteration, delayedAdjacent, true);
// Add diagonal tiles
if (TileLists.TryGetValue(iteration - 3, out var diagonal))
AddNewDiagonalTiles(iteration, diagonal, false);
if (FreedTileLists.TryGetValue(iteration - 3, out var delayedDiagonal))
AddNewDiagonalTiles(iteration, delayedDiagonal, true);
// Add delayed tiles
AddDelayedNeighbors(iteration);
// Tiles from Spaaaace
if (gridJump != null)
{
foreach (var tile in gridJump)
{
ProcessNewTile(iteration, tile, AtmosDirection.Invalid);
}
}
// Store new tiles
if (NewTiles.Count != 0)
TileLists[iteration] = NewTiles;
if (NewBlockedTiles.Count != 0)
BlockedTileLists[iteration] = NewBlockedTiles;
return NewTiles.Count + NewBlockedTiles.Count;
}
protected override void ProcessNewTile(int iteration, Vector2i tile, AtmosDirection entryDirections)
{
// Is there an airtight blocker on this tile?
if (!_airtightMap.TryGetValue(tile, out var tileData))
{
// No blocker. Ezy. Though maybe this a space tile?
if (_spaceTiles.Contains(tile))
JumpToSpace(tile);
else if (ProcessedTiles.Add(tile))
NewTiles.Add(tile);
return;
}
// If the explosion is entering this new tile from an unblocked direction, we add it directly. Note that because
// for space -> grid jumps, we don't have a direction from which the explosion came, we will only assume it is
// unblocked if all space-facing directions are unblocked. Though this could eventually be done properly.
bool blocked;
var blockedDirections = tileData.BlockedDirections;
if (entryDirections == AtmosDirection.Invalid) // is coming from space?
{
var spaceDirections = _edgeTiles[tile];
blocked = (blockedDirections & spaceDirections) != 0; // at least one space direction is blocked.
}
else
blocked = (blockedDirections & entryDirections) == entryDirections;// **ALL** entry directions are blocked
if (blocked)
{
// was this tile already entered from some other direction?
if (EnteredBlockedTiles.Contains(tile))
return;
// Did the explosion already attempt to enter this tile from some other direction?
if (!UnenteredBlockedTiles.Add(tile))
return;
NewBlockedTiles.Add(tile);
// At what explosion iteration would this blocker be destroyed?
if (!tileData.ExplosionTolerance.TryGetValue(_typeID, out var sealIntegrity))
sealIntegrity = float.MaxValue; // indestructible airtight entity
var clearIteration = iteration + (int) MathF.Ceiling(sealIntegrity / _intensityStepSize);
if (FreedTileLists.TryGetValue(clearIteration, out var list))
list.Add(tile);
else
FreedTileLists[clearIteration] = new() { tile };
return;
}
// was this tile already entered from some other direction?
if (!EnteredBlockedTiles.Add(tile))
return;
// Did the explosion already attempt to enter this tile from some other direction?
if (UnenteredBlockedTiles.Contains(tile))
{
NewFreedTiles.Add(tile);
return;
}
// This is a completely new tile, and we just so happened to enter it from an unblocked direction.
NewTiles.Add(tile);
}
private void JumpToSpace(Vector2i tile)
{
// Did we already jump/process this tile?
if (!_processedSpaceTiles.Add(tile))
return;
if (!_needToTransform)
{
SpaceJump.Add(tile);
return;
}
var center = _matrix.Transform(tile);
SpaceJump.Add(new((int) MathF.Floor(center.X + _offset.X), (int) MathF.Floor(center.Y + _offset.Y)));
SpaceJump.Add(new((int) MathF.Floor(center.X - _offset.Y), (int) MathF.Floor(center.Y + _offset.X)));
SpaceJump.Add(new((int) MathF.Floor(center.X - _offset.X), (int) MathF.Floor(center.Y - _offset.Y)));
SpaceJump.Add(new((int) MathF.Floor(center.X + _offset.Y), (int) MathF.Floor(center.Y - _offset.X)));
}
private void AddDelayedNeighbors(int iteration)
{
if (!_delayedNeighbors.TryGetValue(iteration, out var delayed))
return;
foreach (var (tile, direction) in delayed)
{
ProcessNewTile(iteration, tile, direction);
}
_delayedNeighbors.Remove(iteration);
}
// Gets the tiles that are directly adjacent to other tiles. If a currently exploding tile has an airtight entity
// that blocks the explosion from propagating in some direction, those tiles are added to a list of delayed tiles
// that will be added to the explosion in some future iteration.
private void AddNewAdjacentTiles(int iteration, IEnumerable<Vector2i> tiles, bool ignoreTileBlockers = false)
{
foreach (var tile in tiles)
{
var blockedDirections = AtmosDirection.Invalid;
float sealIntegrity = 0;
// Note that if (grid, tile) is not a valid key, then airtight.BlockedDirections will default to 0 (no blocked directions)
if (_airtightMap.TryGetValue(tile, out var tileData))
{
blockedDirections = tileData.BlockedDirections;
if (!tileData.ExplosionTolerance.TryGetValue(_typeID, out sealIntegrity))
sealIntegrity = float.MaxValue; // indestructible airtight entity
}
// First, yield any neighboring tiles that are not blocked by airtight entities on this tile
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
if (ignoreTileBlockers || !blockedDirections.IsFlagSet(direction))
{
ProcessNewTile(iteration, tile.Offset(direction), direction.GetOpposite());
}
}
// If there are no blocked directions, we are done with this tile.
if (ignoreTileBlockers || blockedDirections == AtmosDirection.Invalid)
continue;
// This tile has one or more airtight entities anchored to it blocking the explosion from traveling in
// some directions. First, check whether this blocker can even be destroyed by this explosion?
if (sealIntegrity > _maxIntensity || float.IsNaN(sealIntegrity))
continue;
// At what explosion iteration would this blocker be destroyed?
var clearIteration = iteration + (int) MathF.Ceiling(sealIntegrity / _intensityStepSize);
// Get the delayed neighbours list
if (!_delayedNeighbors.TryGetValue(clearIteration, out var list))
{
list = new();
_delayedNeighbors[clearIteration] = list;
}
// Check which directions are blocked, and add them to the list.
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
if (blockedDirections.IsFlagSet(direction))
{
list.Add((tile.Offset(direction), direction.GetOpposite()));
}
}
}
}
protected override AtmosDirection GetUnblockedDirectionOrAll(Vector2i tile)
{
return ~_airtightMap.GetValueOrDefault(tile).BlockedDirections;
}
}

View File

@@ -1,157 +0,0 @@
using Content.Shared.Atmos;
using Robust.Shared.Map;
namespace Content.Server.Explosion.EntitySystems;
internal sealed class SpaceExplosion : TileExplosion
{
/// <summary>
/// The keys of this dictionary correspond to space tiles that intersect a grid. The values have information
/// about what grid (which could be more than one), and in what directions the space-based explosion is allowed
/// to propagate from this tile.
/// </summary>
private Dictionary<Vector2i, GridBlockData> _gridBlockMap;
/// <summary>
/// After every iteration, this data set will store all the grid-tiles that were reached as a result of the
/// explosion expanding in space.
/// </summary>
internal Dictionary<GridId, HashSet<Vector2i>> GridJump = new();
internal SpaceExplosion(ExplosionSystem system, MapId targetMap, GridId? referenceGrid, List<GridId> localGrids)
{
(_gridBlockMap, var tileSize) = system.TransformGridEdges(targetMap, referenceGrid, localGrids);
system.GetUnblockedDirections(_gridBlockMap, tileSize);
}
internal int AddNewTiles(int iteration, HashSet<Vector2i> inputSpaceTiles)
{
NewTiles = new();
NewBlockedTiles = new();
NewFreedTiles = new();
GridJump = new();
// Adjacent tiles
if (TileLists.TryGetValue(iteration - 2, out var adjacent))
AddNewAdjacentTiles(iteration, adjacent);
if (FreedTileLists.TryGetValue((iteration - 2) % 3, out var delayedAdjacent))
AddNewAdjacentTiles(iteration, delayedAdjacent);
// Diagonal tiles
if (TileLists.TryGetValue(iteration - 3, out var diagonal))
AddNewDiagonalTiles(iteration, diagonal);
if (FreedTileLists.TryGetValue((iteration - 3) % 3, out var delayedDiagonal))
AddNewDiagonalTiles(iteration, delayedDiagonal);
// Tiles entering space from some grid.
foreach (var tile in inputSpaceTiles)
{
ProcessNewTile(iteration, tile, AtmosDirection.All);
}
// Store new tiles
if (NewTiles.Count != 0)
TileLists[iteration] = NewTiles;
if (NewBlockedTiles.Count != 0)
BlockedTileLists[iteration] = NewBlockedTiles;
FreedTileLists[iteration % 3] = NewFreedTiles;
// return new tile count
return NewTiles.Count + NewBlockedTiles.Count;
}
private void JumpToGrid(GridBlockData blocker)
{
foreach (var edge in blocker.BlockingGridEdges)
{
if (edge.Grid == null) continue;
if (!GridJump.TryGetValue(edge.Grid.Value, out var set))
{
set = new();
GridJump[edge.Grid.Value] = set;
}
set.Add(edge.Tile);
}
}
private void AddNewAdjacentTiles(int iteration, IEnumerable<Vector2i> tiles)
{
foreach (var tile in tiles)
{
var unblockedDirections = GetUnblockedDirectionOrAll(tile);
if (unblockedDirections == AtmosDirection.Invalid)
continue;
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
if (!unblockedDirections.IsFlagSet(direction))
continue; // explosion cannot propagate in this direction. Ever.
ProcessNewTile(iteration, tile.Offset(direction), direction.GetOpposite());
}
}
}
internal override void InitTile(Vector2i initialTile)
{
base.InitTile(initialTile);
// It might be the case that the initial space-explosion tile actually overlaps on a grid. In that case we
// need to manually add it to the `spaceToGridTiles` dictionary. This would normally be done automatically
// during the neighbor finding steps.
if (_gridBlockMap.TryGetValue(initialTile, out var blocker))
JumpToGrid(blocker);
}
protected override void ProcessNewTile(int iteration, Vector2i tile, AtmosDirection entryDirection)
{
if (!_gridBlockMap.TryGetValue(tile, out var blocker))
{
// this tile does not intersect any grids. Add it (if its new) and continue.
if (ProcessedTiles.Add(tile))
NewTiles.Add(tile);
return;
}
// Is the entry to this tile blocked?
if ((blocker.UnblockedDirections & entryDirection) == 0)
{
// was this tile already entered from some other direction?
if (EnteredBlockedTiles.Contains(tile))
return;
// Did the explosion already attempt to enter this tile from some other direction?
if (!UnenteredBlockedTiles.Add(tile))
return;
// First time the explosion is reaching this tile.
NewBlockedTiles.Add(tile);
JumpToGrid(blocker);
}
// Was this tile already entered?
if (!EnteredBlockedTiles.Add(tile))
return;
// Did the explosion already attempt to enter this tile from some other direction?
if (UnenteredBlockedTiles.Contains(tile))
{
NewFreedTiles.Add(tile);
return;
}
// This is a completely new tile, and we just so happened to enter it from an unblocked direction.
NewTiles.Add(tile);
JumpToGrid(blocker);
}
protected override AtmosDirection GetUnblockedDirectionOrAll(Vector2i tile)
{
return _gridBlockMap.TryGetValue(tile, out var blocker) ? blocker.UnblockedDirections : AtmosDirection.All;
}
}

View File

@@ -1,116 +0,0 @@
using Content.Shared.Atmos;
namespace Content.Server.Explosion.EntitySystems;
/// <summary>
/// This is the base class for <see cref="SpaceExplosion"/> and <see cref="GridExplosion"/>. It just exists to avoid some code duplication, because those classes are generally quite distinct.
/// </summary>
internal abstract class TileExplosion
{
// Main tile data sets, mapping iterations onto tile lists
internal Dictionary<int, List<Vector2i>> TileLists = new();
protected Dictionary<int, List<Vector2i>> BlockedTileLists = new();
protected Dictionary<int, HashSet<Vector2i>> FreedTileLists = new();
// The new tile lists added each iteration. I **could** just pass these along to every function, but IMO it is more
// readable if they are just private variables.
protected List<Vector2i> NewTiles = default!;
protected List<Vector2i> NewBlockedTiles = default!;
protected HashSet<Vector2i> NewFreedTiles = default!;
// HashSets used to ensure uniqueness of tiles. Prevents the explosion from looping back in on itself.
protected HashSet<Vector2i> ProcessedTiles = new();
protected HashSet<Vector2i> UnenteredBlockedTiles = new();
protected HashSet<Vector2i> EnteredBlockedTiles = new();
internal virtual void InitTile(Vector2i initialTile)
{
ProcessedTiles.Add(initialTile);
TileLists[0] = new() { initialTile };
}
protected abstract void ProcessNewTile(int iteration, Vector2i tile, AtmosDirection entryDirections);
protected abstract AtmosDirection GetUnblockedDirectionOrAll(Vector2i tile);
protected void AddNewDiagonalTiles(int iteration, IEnumerable<Vector2i> tiles, bool ignoreLocalBlocker = false)
{
AtmosDirection entryDirection = AtmosDirection.Invalid;
foreach (var tile in tiles)
{
var freeDirections = ignoreLocalBlocker ? AtmosDirection.All : GetUnblockedDirectionOrAll(tile);
// Get the free directions of the directly adjacent tiles
var freeDirectionsN = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.North));
var freeDirectionsE = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.East));
var freeDirectionsS = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.South));
var freeDirectionsW = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.West));
// North East
if (freeDirections.IsFlagSet(AtmosDirection.North) && freeDirectionsN.IsFlagSet(AtmosDirection.SouthEast))
entryDirection |= AtmosDirection.West;
if (freeDirections.IsFlagSet(AtmosDirection.East) && freeDirectionsE.IsFlagSet(AtmosDirection.NorthWest))
entryDirection |= AtmosDirection.South;
if (entryDirection != AtmosDirection.Invalid)
{
ProcessNewTile(iteration, tile + (1, 1), entryDirection);
entryDirection = AtmosDirection.Invalid;
}
// North West
if (freeDirections.IsFlagSet(AtmosDirection.North) && freeDirectionsN.IsFlagSet(AtmosDirection.SouthWest))
entryDirection |= AtmosDirection.East;
if (freeDirections.IsFlagSet(AtmosDirection.West) && freeDirectionsW.IsFlagSet(AtmosDirection.NorthEast))
entryDirection |= AtmosDirection.West;
if (entryDirection != AtmosDirection.Invalid)
{
ProcessNewTile(iteration, tile + (-1, 1), entryDirection);
entryDirection = AtmosDirection.Invalid;
}
// South East
if (freeDirections.IsFlagSet(AtmosDirection.South) && freeDirectionsS.IsFlagSet(AtmosDirection.NorthEast))
entryDirection |= AtmosDirection.West;
if (freeDirections.IsFlagSet(AtmosDirection.East) && freeDirectionsE.IsFlagSet(AtmosDirection.SouthWest))
entryDirection |= AtmosDirection.North;
if (entryDirection != AtmosDirection.Invalid)
{
ProcessNewTile(iteration, tile + (1, -1), entryDirection);
entryDirection = AtmosDirection.Invalid;
}
// South West
if (freeDirections.IsFlagSet(AtmosDirection.South) && freeDirectionsS.IsFlagSet(AtmosDirection.NorthWest))
entryDirection |= AtmosDirection.West;
if (freeDirections.IsFlagSet(AtmosDirection.West) && freeDirectionsW.IsFlagSet(AtmosDirection.SouthEast))
entryDirection |= AtmosDirection.North;
if (entryDirection != AtmosDirection.Invalid)
{
ProcessNewTile(iteration, tile + (-1, -1), entryDirection);
entryDirection = AtmosDirection.Invalid;
}
}
}
/// <summary>
/// Merge all tile lists into a single output tile list.
/// </summary>
internal void CleanUp()
{
foreach (var (iteration, blocked) in BlockedTileLists)
{
if (TileLists.TryGetValue(iteration, out var tiles))
tiles.AddRange(blocked);
else
TileLists[iteration] = blocked;
}
}
}

View File

@@ -65,11 +65,33 @@ 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)
{ {
_explosions.TriggerExplosive(uid); if (!EntityManager.TryGetComponent(uid, out ExplosiveComponent? explosiveComponent)) return;
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)
{ {

View File

@@ -50,8 +50,6 @@ 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();
@@ -140,11 +138,8 @@ namespace Content.Server.NodeContainer.EntitySystems
{ {
base.Update(frameTime); base.Update(frameTime);
if (!Snoozing) DoGroupUpdates();
{ VisDoUpdate(frameTime);
DoGroupUpdates();
VisDoUpdate(frameTime);
}
} }
private void DoGroupUpdates() private void DoGroupUpdates()

View File

@@ -1,9 +1,7 @@
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
{ {
@@ -38,6 +36,13 @@ 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>
@@ -62,48 +67,6 @@ 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>

View File

@@ -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 ExplosionSystem _explosions = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly IChatManager _chat = default!;
public override void Initialize() public override void Initialize()
@@ -397,16 +397,19 @@ namespace Content.Server.Nuke
if (!Resolve(uid, ref component, ref transform)) if (!Resolve(uid, ref component, ref transform))
return; return;
if (component.Exploded) // gib anyone in a blast radius
return; // its lame, but will work for now
var pos = transform.Coordinates;
var ents = _lookup.GetEntitiesInRange(pos, component.BlastRadius);
foreach (var ent in ents)
{
var entUid = ent;
if (!EntityManager.EntityExists(entUid))
continue;
component.Exploded = true; if (EntityManager.TryGetComponent(entUid, out SharedBodyComponent? body))
body.Gib();
_explosions.QueueExplosion(uid, }
component.ExplosionType,
component.TotalIntensity,
component.IntensitySlope,
component.MaxIntensity);
EntityManager.DeleteEntity(uid); EntityManager.DeleteEntity(uid);
} }

View File

@@ -1,5 +1,6 @@
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;
@@ -25,5 +26,8 @@ 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");
} }
} }

View File

@@ -5,6 +5,7 @@ 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;
@@ -18,7 +19,8 @@ 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()
{ {
@@ -101,7 +103,10 @@ namespace Content.Server.Pointing.EntitySystems
{ {
return; return;
} }
_explosion.QueueExplosion(uid, "Default", 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);
} }
} }

View File

@@ -88,10 +88,10 @@ public sealed class PowerCellSystem : SharedPowerCellSystem
if (!Resolve(uid, ref battery)) if (!Resolve(uid, ref battery))
return; return;
var radius = MathF.Min(5, MathF.Ceiling(MathF.Sqrt(battery.CurrentCharge) / 30)); var heavy = (int) Math.Ceiling(Math.Sqrt(battery.CurrentCharge) / 60);
battery.CurrentCharge = 0; var light = (int) Math.Ceiling(Math.Sqrt(battery.CurrentCharge) / 30);
_explosionSystem.TriggerExplosive(uid, radius: radius); _explosionSystem.SpawnExplosion(uid, 0, heavy, light, light * 2);
QueueDel(uid); QueueDel(uid);
} }

View File

@@ -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 public class EntityStorageComponent : Component, IActivate, IStorageComponent, IInteractUsing, IDestroyAct, IExAct
{ {
[Dependency] private readonly IEntityManager _entMan = default!; [Dependency] private readonly IEntityManager _entMan = default!;
@@ -478,6 +478,24 @@ 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

View File

@@ -43,7 +43,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, IAfterInteract public sealed class ServerStorageComponent : SharedStorageComponent, IInteractUsing, IActivate, IStorageComponent, IDestroyAct, IExAct, IAfterInteract
{ {
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
@@ -667,6 +667,30 @@ 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);

View File

@@ -29,6 +29,21 @@ 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
{ {
@@ -44,7 +59,23 @@ namespace Content.Shared.Acts
destroyAct.OnDestroy(eventArgs); destroyAct.OnDestroy(eventArgs);
} }
QueueDel(owner); EntityManager.QueueDeleteEntity(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)
@@ -58,4 +89,11 @@ namespace Content.Shared.Acts
} }
} }
} }
public enum ExplosionSeverity
{
Light,
Heavy,
Destruction,
}
} }

View File

@@ -1,52 +0,0 @@
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;
}
}
}

View File

@@ -378,85 +378,6 @@ 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.tilespertick", 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.throwlimit", 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.nodesleep", 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.maxarea", (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.maxiterations", 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.maxtime", 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.incrementaltile", false, CVar.SERVERONLY);
/* /*
* Admin logs * Admin logs
*/ */

View File

@@ -25,7 +25,7 @@ namespace Content.Shared.Damage
[RegisterComponent] [RegisterComponent]
[NetworkedComponent()] [NetworkedComponent()]
[Friend(typeof(DamageableSystem))] [Friend(typeof(DamageableSystem))]
public sealed class DamageableComponent : Component, IRadiationAct public sealed class DamageableComponent : Component, IRadiationAct, IExAct
{ {
/// <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,6 +92,27 @@ 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]

View File

@@ -1,81 +0,0 @@
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 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 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 byte ExplosionId;
public ExplosionEvent(
byte explosionId,
MapCoordinates epicenter,
string typeID,
List<float> intensity,
Dictionary<int, List<Vector2i>>? spaceTiles,
Dictionary<GridId, Dictionary<int, List<Vector2i>>> tiles,
Matrix3 spaceMatrix)
{
Epicenter = epicenter;
SpaceTiles = spaceTiles;
Tiles = tiles;
Intensity = intensity;
TypeID = typeID;
SpaceMatrix = spaceMatrix;
ExplosionId = explosionId;
}
}
/// <summary>
/// Update visual rendering of the explosion to correspond to the servers processing of it.
/// </summary>
[Serializable, NetSerializable]
public class ExplosionOverlayUpdateEvent : EntityEventArgs
{
public int Index;
public byte ExplosionId;
public ExplosionOverlayUpdateEvent(byte explosionId, int index)
{
Index = index;
ExplosionId = explosionId;
}
}

View File

@@ -1,89 +0,0 @@
using Content.Shared.Damage;
using Content.Shared.Sound;
using Robust.Shared.Prototypes;
namespace Content.Shared.Explosion;
[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 string TexturePath = "/Textures/Effects/fire.rsi";
// 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 _tileBreakChance and _tileBreakIntensity
/// </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]);
}
}

View File

@@ -234,11 +234,6 @@ namespace Content.Shared.FixedPoint
_value == unit._value; _value == unit._value;
} }
public bool EqualsApprox(FixedPoint2 other, FixedPoint2 tolerance)
{
return Math.Abs(_value - other._value) < tolerance._value;
}
public override readonly int GetHashCode() public override readonly int GetHashCode()
{ {
// ReSharper disable once NonReadonlyMemberInGetHashCode // ReSharper disable once NonReadonlyMemberInGetHashCode

View File

@@ -1,10 +1,38 @@
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 public abstract class InventoryComponent : Component, IExAct
{ {
[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);
}
}
}
}
} }

View File

@@ -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,7 +14,6 @@ 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

View File

@@ -1,16 +0,0 @@
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!

View File

@@ -9,8 +9,6 @@
- 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

View File

@@ -8,15 +8,13 @@
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 #explosion damage ignores "normal" armor, see ExplosionResistance. - type: Armor
modifiers: modifiers:
coefficients: coefficients:
Blunt: 0.5 Blunt: 0.2 #big blast protection, a little protection from heat, negligable cushioning from other damage cos its thick
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

View File

@@ -1,9 +0,0 @@
- type: entity
abstract: true
id: ExplosionLight
name: explosion light
components:
- type: PointLight
- type: Tag
tags:
- HideContextMenu

View File

@@ -21,11 +21,6 @@
layer: layer:
- SmallImpassable - SmallImpassable
- type: Nuke - type: Nuke
# ~35 tile radius in open space
explosionType: Default
maxIntensity: 100
intensitySlope: 5
totalIntensity: 200000
diskSlot: diskSlot:
name: Disk name: Disk
insertSound: insertSound:

View File

@@ -8,8 +8,6 @@
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

View File

@@ -21,9 +21,6 @@
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

View File

@@ -18,12 +18,10 @@
damage: {} damage: {}
deleteOnCollide: false deleteOnCollide: false
- type: Explosive - type: Explosive
explosionType: Default devastationRange: 3
totalIntensity: 2000.0 heavyImpactRange: 5
intensitySlope: 20 lightImpactRange: 7
maxIntensity: 60 flashRange: 10
# should be enough to kill reinforced walls, so meteors are actually a problem for the station.
# though maybe reinforced walls should just be nerfed?
- type: Physics - type: Physics
bodyType: Dynamic bodyType: Dynamic
fixedRotation: false fixedRotation: false
@@ -31,7 +29,7 @@
fixtures: fixtures:
- shape: - shape:
!type:PhysShapeCircle !type:PhysShapeCircle
radius: 1.0 radius: 0.5
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.

View File

@@ -187,10 +187,10 @@
state: frag state: frag
- type: ExplodeOnTrigger - type: ExplodeOnTrigger
- type: Explosive - type: Explosive
explosionType: Default devastationRange: 1
maxIntensity: 50 heavyImpactRange: 2
intensitySlope: 7.5 lightImpactRange: 4
totalIntensity: 300 flashRange: 10
- type: PointLight - type: PointLight
radius: 3.5 radius: 3.5
color: orange color: orange
@@ -229,9 +229,10 @@
state: grenade state: grenade
- type: ExplodeOnTrigger - type: ExplodeOnTrigger
- type: Explosive - type: Explosive
explosionType: Default devastationRange: 1
totalIntensity: 500 # a ~7 tile radius. This is pretty big. heavyImpactRange: 2
maxIntensity: 10 # caps at 150 damage lightImpactRange: 7
flashRange: 10
- type: entity - type: entity
id: BulletGrenadeFlash id: BulletGrenadeFlash
@@ -259,8 +260,10 @@
state: grenade state: grenade
- type: ExplodeOnTrigger - type: ExplodeOnTrigger
- type: Explosive - type: Explosive
explosionType: Default devastationRange: 0
totalIntensity: 64 # about a ~4 tile radius heavyImpactRange: 1
lightImpactRange: 4
flashRange: 10
- type: entity - type: entity
id: BulletFoam id: BulletFoam

View File

@@ -18,10 +18,10 @@
- type: OnUseTimerTrigger - type: OnUseTimerTrigger
delay: 3.5 delay: 3.5
- type: Explosive - type: Explosive
explosionType: Default devastationRange: 0
maxIntensity: 10 heavyImpactRange: 2
intensitySlope: 3 lightImpactRange: 4
totalIntensity: 120 # about a ~4 tile radius flashRange: 7
- type: ExplodeOnTrigger - type: ExplodeOnTrigger
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
@@ -94,10 +94,10 @@
- type: OnUseTimerTrigger - type: OnUseTimerTrigger
delay: 5 delay: 5
- type: Explosive - type: Explosive
explosionType: Default devastationRange: 1
totalIntensity: 400 heavyImpactRange: 3
intensitySlope: 3 lightImpactRange: 5
maxIntensity: 20 flashRange: 10
- type: ExplodeOnTrigger - type: ExplodeOnTrigger
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
@@ -129,10 +129,9 @@
- type: OnUseTimerTrigger - type: OnUseTimerTrigger
delay: 5 delay: 5
- type: Explosive - type: Explosive
explosionType: Default devastationRange: 25
totalIntensity: 20000 # ~15 tile radius. heavyImpactRange: 25
intensitySlope: 5 flashRange: 50
maxIntensity: 50
- type: ExplodeOnTrigger - type: ExplodeOnTrigger
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic

View File

@@ -46,10 +46,6 @@
- type: Anchorable - type: Anchorable
- type: Pullable - type: Pullable
- type: AMEController - type: AMEController
- type: Explosive
explosionType: Default
intensitySlope: 5
maxIntensity: 60
- type: UserInterface - type: UserInterface
interfaces: interfaces:
- key: enum.AMEControllerUiKey.Key - key: enum.AMEControllerUiKey.Key

View File

@@ -64,10 +64,10 @@
min: 1 min: 1
max: 1 max: 1
- type: Explosive - type: Explosive
explosionType: Default devastationRange: 1
maxIntensity: 100 heavyImpactRange: 3
intensitySlope: 2 lightImpactRange: 5
totalIntensity: 200 flashRange: 6
- type: Wires - type: Wires
BoardName: "Substation" BoardName: "Substation"
LayoutId: Substation LayoutId: Substation

View File

@@ -167,8 +167,6 @@
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

View File

@@ -19,8 +19,12 @@
types: types:
Heat: 10 Heat: 10
- type: Explosive - type: Explosive
explosionType: Default # The explosive component is independent of the stored liquid :/
totalIntensity: 120 # ~ 5 tile radius # I dream of a day where all grenades/explosives will be reagent dependent.
devastationRange: 0
heavyImpactRange: 2
lightImpactRange: 6
flashRange: 5
- type: entity - type: entity
id: WeldingFuelTankFull id: WeldingFuelTankFull

View File

@@ -23,6 +23,7 @@
- SmallImpassable - SmallImpassable
- type: Tag - type: Tag
tags: tags:
- ExplosivePassable
- Wooden - Wooden
- type: Damageable - type: Damageable
damageModifierSet: Wood damageModifierSet: Wood

View File

@@ -25,6 +25,9 @@
- 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

View File

@@ -88,11 +88,13 @@
amount: 1 amount: 1
effects: effects:
- !type:ExplosionReactionEffect - !type:ExplosionReactionEffect
explosionType: Default #Ranges used when 1 potassium + 1 water react (A unit reaction)
maxIntensity: 1 # at most 15 damage per tile devastationRange: 0.025
intensityPerUnit: 0.5 # 50+50 reagent for maximum explosion heavyImpactRange: 0.05
intensitySlope: 2 lightImpactRange: 0.075
maxTotalIntensity: 25 flashRange: 0.1
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

View File

@@ -37,14 +37,12 @@
amount: 3 amount: 3
effects: effects:
# TODO electro's pretty explosions PR make this big and firey!! # TODO electro's pretty explosions PR make this big and firey!!
# Welp its fiery now (low damage). Someone else needs to come in and define how big it should get & how it compares to potassium
# TODO solution temperature!! # TODO solution temperature!!
- !type:ExplosionReactionEffect - !type:ExplosionReactionEffect
explosionType: Default # 15 damage per intensity devastationRange: 0
maxIntensity: 2 # at most 30 damage per tile. heavyImpactRange: 0
intensityPerUnit: 2 # 80 total input reagent units for max size lightImpactRange: 2
intensitySlope: 1 scaled: false
maxTotalIntensity: 40
- !type:PopupMessage - !type:PopupMessage
messages: [ "clf3-explosion" ] messages: [ "clf3-explosion" ]
type: Pvs type: Pvs

View File

@@ -1,39 +0,0 @@
- 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1 +0,0 @@
{"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]]}]}