From cc4669244da01d18cf4833ae6df65618353b9f13 Mon Sep 17 00:00:00 2001 From: Daniel Castro Razo Date: Sat, 2 Jan 2021 12:03:10 -0600 Subject: [PATCH] Fixes to explosionhelper (#2819) * Revert "Make handheld explosives affect tiles (#2806)" This reverts commit 005e142949e4c209c2e1ebbf27f0c674ed9b31c8. * Fixes tiles being destroyed under walls by an explosion * Extra imports removed * Handles explosion in space and different grids This handle explosions across different grids, and tiles are still protected if there is an entity airtight and currently blocking air on top of them that survived the explosion. * Some bug fixes - The way tiles were being protected was silly. - Big explosions cause a lot of objects to trigger multiple events and at the same time they are destroyed. - Explosions spawning inside containers like closets work now. * Range bug fixes * Explosive The explosion works even if the entity exploding is inside multiple 'layers' of containers like. bomb -> survival box -> tool box -> closet * Explosions are different now Explosion can't jump over walls now. Explosions work like rays now, if an explosion breaks a wall it can scatter inside the room. If entities are behind impassable entities that survive the blast they are left unscathed. * Little fix * Remove the extra lookup of tiles * Another small change * Restore the second lookup I thought this was extra, but this protects the tile under it if there is an Impassable entity on top. None wants anchored girders on top of lattice/space * Changing order of conditions IsBlockedTurf is cheaper to run than InRangeUnobstructed. * Yep --- Content.Server/Explosions/ExplosionHelper.cs | 336 +++++++++++++----- .../Explosion/ExplosiveComponent.cs | 23 +- .../Items/Storage/EntityStorageComponent.cs | 5 +- .../Projectiles/ThrownItemComponent.cs | 2 +- Content.Server/Throw/ThrowHelper.cs | 15 +- Content.Shared/Physics/CollisionGroup.cs | 3 +- .../Entities/Constructible/Walls/girder.yml | 1 + 7 files changed, 281 insertions(+), 104 deletions(-) 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