diff --git a/Content.Server/Explosions/ExplosionHelper.cs b/Content.Server/Explosions/ExplosionHelper.cs index fd7abd3a90..5479db7b3a 100644 --- a/Content.Server/Explosions/ExplosionHelper.cs +++ b/Content.Server/Explosions/ExplosionHelper.cs @@ -1,20 +1,33 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; +using Content.Server.GameObjects.Components.Atmos; +using Content.Server.GameObjects.Components.Explosion; +using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Mobs; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Maps; +using Content.Shared.Physics; +using Content.Shared.Utility; +using Microsoft.Extensions.Logging; using Robust.Server.GameObjects.EntitySystems; using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.Player; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components; using Robust.Shared.GameObjects.EntitySystemMessages; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Random; using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; +using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; +using Robust.Shared.Physics; using Robust.Shared.Random; +using Robust.Shared.Timing; namespace Content.Server.Explosions { @@ -26,129 +39,211 @@ namespace Content.Server.Explosions /// private static readonly Vector2 EpicenterDistance = (0.1f, 0.1f); - public static void SpawnExplosion(this EntityCoordinates coords, int devastationRange, int heavyImpactRange, int lightImpactRange, int flashRange) + /// + /// Chance of a tile breaking if the severity is Light and Heavy + /// + private static readonly float LightBreakChance = 0.3f; + private static readonly float HeavyBreakChance = 0.8f; + + private static bool IgnoreExplosivePassable(IEntity e) => (e.GetComponent().CollisionLayer & (int) CollisionGroup.ExplosivePassable) != 0; + + private static ExplosionSeverity CalculateSeverity(float distance, float devastationRange, float heaveyRange) { - var tileDefinitionManager = IoCManager.Resolve(); + if (distance < devastationRange) + { + return ExplosionSeverity.Destruction; + } + else if (distance < heaveyRange) + { + return ExplosionSeverity.Heavy; + } + else + { + return ExplosionSeverity.Light; + } + } + + /// + /// Damage entities inside the range. The damage depends on a discrete + /// damage bracket [light, heavy, devastation] and the distance from the epicenter + /// + /// + /// A dictionary of coordinates relative to the parents of every grid of entities that survived the explosion, + /// have an airtight component and are currently blocking air. Like a wall. + /// + private static void DamageEntitiesInRange(EntityCoordinates epicenter, Box2 boundingBox, + float devastationRange, + float heaveyRange, + float maxRange, + MapId mapId) + { + var entityManager = IoCManager.Resolve(); var serverEntityManager = IoCManager.Resolve(); var entitySystemManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); - var robustRandom = IoCManager.Resolve(); - var entityManager = IoCManager.Resolve(); - var maxRange = MathHelper.Max(devastationRange, heavyImpactRange, lightImpactRange, 0f); - //Entity damage calculation - var entitiesAll = serverEntityManager.GetEntitiesInRange(coords, maxRange).ToList(); + var exAct = entitySystemManager.GetEntitySystem(); - foreach (var entity in entitiesAll) + var entitiesInRange = serverEntityManager.GetEntitiesInRange(mapId, boundingBox, 0).ToList(); + + var impassableEntities = new List>(); + var nonImpassableEntities = new List>(); + + // The entities are paired with their distance to the epicenter + // and splitted into two lists based on if they are Impassable or not + foreach (var entity in entitiesInRange) { - if (entity.Deleted) - continue; - if (!entity.Transform.IsMapTransform) - continue; - - if (!entity.Transform.Coordinates.TryDistance(entityManager, coords, out var distance)) + if (entity.Deleted || !entity.Transform.IsMapTransform) { continue; } - ExplosionSeverity severity; - if (distance < devastationRange) + if (!entity.Transform.Coordinates.TryDistance(entityManager, epicenter, out var distance) || distance > maxRange) { - severity = ExplosionSeverity.Destruction; + continue; } - else if (distance < heavyImpactRange) + + if (!entity.TryGetComponent(out IPhysicsComponent body) || body.PhysicsShapes.Count < 1) { - severity = ExplosionSeverity.Heavy; + continue; } - else if (distance < lightImpactRange) + + if ((body.CollisionLayer & (int) CollisionGroup.Impassable) != 0) { - severity = ExplosionSeverity.Light; + impassableEntities.Add(Tuple.Create(entity, distance)); } else + { + nonImpassableEntities.Add(Tuple.Create(entity, distance)); + } + } + + // The Impassable entities are sorted in descending order + // Entities closer to the epicenter are first + impassableEntities.Sort((x, y) => x.Item2.CompareTo(y.Item2)); + + // Impassable entities are handled first. If they are damaged enough, they are destroyed and they may + // be able to spawn a new entity. I.e Wall -> Girder. + // Girder has a layer ExplosivePassable, and the predicate make it so the entities with this layer are ignored + var epicenterMapPos = epicenter.ToMap(entityManager); + foreach (var (entity, distance) in impassableEntities) + { + if (!entity.InRangeUnobstructed(epicenterMapPos, maxRange, ignoreInsideBlocker: true, predicate: IgnoreExplosivePassable)) { continue; } - var exAct = entitySystemManager.GetEntitySystem(); - //exAct.HandleExplosion(Owner, entity, severity); - exAct.HandleExplosion(coords, entity, severity); + exAct.HandleExplosion(epicenter, entity, CalculateSeverity(distance, devastationRange, heaveyRange)); } - //Tile damage calculation mockup - //TODO: make it into some sort of actual damage component or whatever the boys think is appropriate - if (mapManager.TryGetGrid(coords.GetGridId(entityManager), out var mapGrid)) + // 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) { - var circle = new Circle(coords.ToMapPos(entityManager), maxRange); - var tiles = mapGrid?.GetTilesIntersecting(circle); - foreach (var tile in tiles) + if (!entity.InRangeUnobstructed(epicenterMapPos, maxRange, ignoreInsideBlocker: true, predicate: IgnoreExplosivePassable)) { - var tileLoc = mapGrid.GridTileToLocal(tile.GridIndices); - var tileDef = (ContentTileDefinition) tileDefinitionManager[tile.Tile.TypeId]; - var baseTurfs = tileDef.BaseTurfs; - if (baseTurfs.Count == 0) - { - continue; - } + continue; + } + exAct.HandleExplosion(epicenter, entity, CalculateSeverity(distance, devastationRange, heaveyRange)); + } + } - if (!tileLoc.TryDistance(entityManager, coords, out var distance)) - { - continue; - } + /// + /// Damage tiles inside the range. The type of tile can change depending on a discrete + /// damage bracket [light, heavy, devastation], the distance from the epicenter and + /// a probabilty bracket [, , 1.0]. + /// + /// + private static void DamageTilesInRange(EntityCoordinates epicenter, + GridId gridId, + Box2 boundingBox, + float devastationRange, + float heaveyRange, + float maxRange) + { + var mapManager = IoCManager.Resolve(); + if (!mapManager.TryGetGrid(gridId, out var mapGrid)) + { + return; + } - var zeroTile = new Tile(tileDefinitionManager[baseTurfs[0]].TileId); - var previousTile = new Tile(tileDefinitionManager[baseTurfs[^1]].TileId); + var entityManager = IoCManager.Resolve(); + if (!entityManager.TryGetEntity(mapGrid.GridEntityId, out var grid)) + { + return; + } - switch (distance) - { - case var d when d < devastationRange: - mapGrid.SetTile(tileLoc, zeroTile); - break; - case var d when d < heavyImpactRange - && !previousTile.IsEmpty - && robustRandom.Prob(0.8f): + var robustRandom = IoCManager.Resolve(); + var tileDefinitionManager = IoCManager.Resolve(); + + var tilesInGridAndCircle = mapGrid.GetTilesIntersecting(boundingBox); + + var epicenterMapPos = epicenter.ToMap(entityManager); + foreach (var tile in tilesInGridAndCircle) + { + var tileLoc = mapGrid.GridTileToLocal(tile.GridIndices); + if (!tileLoc.TryDistance(entityManager, epicenter, out var distance) || distance > maxRange) + { + continue; + } + + if (tile.IsBlockedTurf(false)) + { + continue; + } + + if (!tileLoc.ToMap(entityManager).InRangeUnobstructed(epicenterMapPos, maxRange, ignoreInsideBlocker: false, predicate: IgnoreExplosivePassable)) + { + continue; + } + + var tileDef = (ContentTileDefinition) tileDefinitionManager[tile.Tile.TypeId]; + var baseTurfs = tileDef.BaseTurfs; + if (baseTurfs.Count == 0) + { + continue; + } + + var zeroTile = new Tile(tileDefinitionManager[baseTurfs[0]].TileId); + var previousTile = new Tile(tileDefinitionManager[baseTurfs[^1]].TileId); + + var severity = CalculateSeverity(distance, devastationRange, heaveyRange); + + switch (severity) + { + case ExplosionSeverity.Light: + if (!previousTile.IsEmpty && robustRandom.Prob(LightBreakChance)) + { mapGrid.SetTile(tileLoc, previousTile); - break; - case var d when d < lightImpactRange - && !previousTile.IsEmpty - && robustRandom.Prob(0.5f): + } + break; + case ExplosionSeverity.Heavy: + if (!previousTile.IsEmpty && robustRandom.Prob(HeavyBreakChance)) + { mapGrid.SetTile(tileLoc, previousTile); - break; - } + } + break; + case ExplosionSeverity.Destruction: + mapGrid.SetTile(tileLoc, zeroTile); + break; } } + } - //Effects and sounds - var time = IoCManager.Resolve().CurTime; - var message = new EffectSystemMessage - { - EffectSprite = "Effects/explosion.rsi", - RsiState = "explosionfast", - Born = time, - DeathTime = time + TimeSpan.FromSeconds(5), - Size = new Vector2(flashRange / 2, flashRange / 2), - Coordinates = coords, - //Rotated from east facing - Rotation = 0f, - ColorDelta = new Vector4(0, 0, 0, -1500f), - Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), 0.5f), - Shaded = false - }; - entitySystemManager.GetEntitySystem().CreateParticle(message); - entitySystemManager.GetEntitySystem().PlayAtCoords("/Audio/Effects/explosion.ogg", coords); - - // Knock back cameras of all players in the area. - + private static void CameraShakeInRange(EntityCoordinates epicenter, float maxRange) + { var playerManager = IoCManager.Resolve(); - foreach (var player in playerManager.GetAllPlayers()) + var players = playerManager.GetPlayersInRange(epicenter, (int) Math.Ceiling(maxRange)); + foreach (var player in players) { - if (player.AttachedEntity == null - || player.AttachedEntity.Transform.MapID != mapGrid.ParentMapId - || !player.AttachedEntity.TryGetComponent(out CameraRecoilComponent recoil)) + if (player.AttachedEntity == null || !player.AttachedEntity.TryGetComponent(out CameraRecoilComponent recoil)) { continue; } + var entityManager = IoCManager.Resolve(); + var playerPos = player.AttachedEntity.Transform.WorldPosition; - var delta = coords.ToMapPos(entityManager) - playerPos; + var delta = epicenter.ToMapPos(entityManager) - playerPos; + //Change if zero. Will result in a NaN later breaking camera shake if not changed if (delta.EqualsApprox((0.0f, 0.0f))) delta = EpicenterDistance; @@ -163,10 +258,75 @@ namespace Content.Server.Explosions } } - public static void SpawnExplosion(this IEntity entity, int devastationRange, int heavyImpactRange, - int lightImpactRange, int flashRange) + private static void FlashInRange(EntityCoordinates epicenter, float flashrange) { - entity.Transform.Coordinates.SpawnExplosion(devastationRange, heavyImpactRange, lightImpactRange, flashRange); + if (flashrange > 0) + { + var entitySystemManager = IoCManager.Resolve(); + var time = IoCManager.Resolve().CurTime; + var message = new EffectSystemMessage + { + EffectSprite = "Effects/explosion.rsi", + RsiState = "explosionfast", + Born = time, + DeathTime = time + TimeSpan.FromSeconds(5), + Size = new Vector2(flashrange / 2, flashrange / 2), + Coordinates = epicenter, + Rotation = 0f, + ColorDelta = new Vector4(0, 0, 0, -1500f), + Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), 0.5f), + Shaded = false + }; + entitySystemManager.GetEntitySystem().CreateParticle(message); + } + } + + private static void Detonate(IEntity source, int devastationRange, int heavyImpactRange, int lightImpactRange, int flashRange) + { + var mapId = source.Transform.MapID; + if (mapId == MapId.Nullspace) + { + return; + } + + var maxRange = MathHelper.Max(devastationRange, heavyImpactRange, lightImpactRange, 0); + + while(source.TryGetContainer(out var cont)) + { + source = cont.Owner; + } + var epicenter = source.Transform.Coordinates; + + var entityManager = IoCManager.Resolve(); + var mapManager = IoCManager.Resolve(); + + var epicenterMapPos = epicenter.ToMapPos(entityManager); + var boundingBox = new Box2(epicenterMapPos - new Vector2(maxRange, maxRange), epicenterMapPos + new Vector2(maxRange, maxRange)); + + DamageEntitiesInRange(epicenter, boundingBox, devastationRange, heavyImpactRange, maxRange, mapId); + + var mapGridsNear = mapManager.FindGridsIntersecting(mapId, boundingBox); + + foreach (var gridId in mapGridsNear) + { + DamageTilesInRange(epicenter, gridId.Index, boundingBox, devastationRange, heavyImpactRange, maxRange); + } + + CameraShakeInRange(epicenter, maxRange); + FlashInRange(epicenter, flashRange); + } + + public static void SpawnExplosion(this IEntity entity, int devastationRange = 0, int heavyImpactRange = 0, int lightImpactRange = 0, int flashRange = 0) + { + // If you want to directly set off the explosive + if (!entity.Deleted && entity.TryGetComponent(out ExplosiveComponent explosive) && !explosive.Exploding) + { + explosive.Explosion(); + } + else + { + Detonate(entity, devastationRange, heavyImpactRange, lightImpactRange, flashRange); + } } } } diff --git a/Content.Server/GameObjects/Components/Explosion/ExplosiveComponent.cs b/Content.Server/GameObjects/Components/Explosion/ExplosiveComponent.cs index 69d0c14772..8b8ab5227a 100644 --- a/Content.Server/GameObjects/Components/Explosion/ExplosiveComponent.cs +++ b/Content.Server/GameObjects/Components/Explosion/ExplosiveComponent.cs @@ -1,4 +1,4 @@ -using Content.Server.Explosions; +using Content.Server.Explosions; using Content.Server.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems; using Robust.Shared.GameObjects; @@ -16,7 +16,7 @@ namespace Content.Server.GameObjects.Components.Explosion public int LightImpactRange = 0; public int FlashRange = 0; - private bool _beingExploded = false; + public bool Exploding { get; private set; } = false; public override void ExposeData(ObjectSerializer serializer) { @@ -30,14 +30,17 @@ namespace Content.Server.GameObjects.Components.Explosion public bool Explosion() { - //Prevent adjacent explosives from infinitely blowing each other up. - if (_beingExploded) return true; - _beingExploded = true; - - Owner.SpawnExplosion(DevastationRange, HeavyImpactRange, LightImpactRange, FlashRange); - - Owner.Delete(); - return true; + if (Exploding) + { + return false; + } + else + { + Exploding = true; + Owner.SpawnExplosion(DevastationRange, HeavyImpactRange, LightImpactRange, FlashRange); + Owner.Delete(); + return true; + } } bool ITimerTrigger.Trigger(TimerTriggerEventArgs eventArgs) diff --git a/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs b/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs index 632bfd4402..7d1bc4a495 100644 --- a/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs +++ b/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Linq; using System.Threading; @@ -470,7 +470,8 @@ namespace Content.Server.GameObjects.Components.Items.Storage return; } - foreach (var entity in Contents.ContainedEntities) + var containedEntities = Contents.ContainedEntities.ToList(); + foreach (var entity in containedEntities) { var exActs = entity.GetAllComponents().ToArray(); foreach (var exAct in exActs) diff --git a/Content.Server/GameObjects/Components/Projectiles/ThrownItemComponent.cs b/Content.Server/GameObjects/Components/Projectiles/ThrownItemComponent.cs index 8caf6ca3c6..089a422711 100644 --- a/Content.Server/GameObjects/Components/Projectiles/ThrownItemComponent.cs +++ b/Content.Server/GameObjects/Components/Projectiles/ThrownItemComponent.cs @@ -34,7 +34,7 @@ namespace Content.Server.GameObjects.Components.Projectiles void ICollideBehavior.CollideWith(IEntity entity) { - if (!_shouldCollide) return; + if (!_shouldCollide || entity.Deleted) return; if (entity.TryGetComponent(out PhysicsComponent collid)) { if (!collid.Hard) // ignore non hard diff --git a/Content.Server/Throw/ThrowHelper.cs b/Content.Server/Throw/ThrowHelper.cs index 4d760383f7..a0ab566592 100644 --- a/Content.Server/Throw/ThrowHelper.cs +++ b/Content.Server/Throw/ThrowHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using Content.Server.GameObjects.Components.Projectiles; using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.EntitySystems; @@ -43,10 +43,21 @@ namespace Content.Server.Throw /// public static void Throw(this IEntity thrownEnt, float throwForce, EntityCoordinates targetLoc, EntityCoordinates sourceLoc, bool spread = false, IEntity throwSourceEnt = null) { + if (thrownEnt.Deleted) + { + return; + } + if (!thrownEnt.TryGetComponent(out IPhysicsComponent colComp)) return; var entityManager = IoCManager.Resolve(); + var direction_vector = targetLoc.ToMapPos(entityManager) - sourceLoc.ToMapPos(entityManager); + + if (direction_vector.Length == 0) + { + return; + } colComp.CanCollide = true; // I can now collide with player, so that i can do damage. @@ -61,8 +72,8 @@ namespace Content.Server.Throw colComp.PhysicsShapes[0].CollisionMask |= (int) CollisionGroup.ThrownItem; colComp.Status = BodyStatus.InAir; } - var angle = new Angle(targetLoc.ToMapPos(entityManager) - sourceLoc.ToMapPos(entityManager)); + var angle = new Angle(direction_vector); if (spread) { var spreadRandom = IoCManager.Resolve(); diff --git a/Content.Shared/Physics/CollisionGroup.cs b/Content.Shared/Physics/CollisionGroup.cs index c5d2448076..5b676e8432 100644 --- a/Content.Shared/Physics/CollisionGroup.cs +++ b/Content.Shared/Physics/CollisionGroup.cs @@ -1,4 +1,4 @@ -using System; +using System; using JetBrains.Annotations; using Robust.Shared.Map; using Robust.Shared.Serialization; @@ -23,6 +23,7 @@ namespace Content.Shared.Physics GhostImpassable = 1 << 6, // 64 Things impassible by ghosts/observers, ie blessed tiles or forcefields Underplating = 1 << 7, // 128 Things that are under plating Passable = 1 << 8, // 256 Things that are passable + ExplosivePassable = 1 << 9, // 512 Things that let the pressure of a explosion through MapGrid = MapGridHelpers.CollisionGroup, // Map grids, like shuttles. This is the actual grid itself, not the walls or other entities connected to the grid. MobMask = Impassable | MobImpassable | VaultImpassable | SmallImpassable, diff --git a/Resources/Prototypes/Entities/Constructible/Walls/girder.yml b/Resources/Prototypes/Entities/Constructible/Walls/girder.yml index 2bdd1fbaee..aafac7939d 100644 --- a/Resources/Prototypes/Entities/Constructible/Walls/girder.yml +++ b/Resources/Prototypes/Entities/Constructible/Walls/girder.yml @@ -24,6 +24,7 @@ - MobImpassable - VaultImpassable - SmallImpassable + - ExplosivePassable - type: Pullable - type: Damageable resistances: metallicResistances