diff --git a/Content.Client/Singularity/SingularitySystem.cs b/Content.Client/Singularity/SingularitySystem.cs index 2454fa487c..2790a37e4d 100644 --- a/Content.Client/Singularity/SingularitySystem.cs +++ b/Content.Client/Singularity/SingularitySystem.cs @@ -2,7 +2,7 @@ namespace Content.Client.Singularity { - public class SingularitySystem : SharedSingularitySystem + public sealed class SingularitySystem : SharedSingularitySystem { } } diff --git a/Content.Server/Physics/Controllers/SingularityController.cs b/Content.Server/Physics/Controllers/SingularityController.cs index eb905f468d..cf3bed3c6d 100644 --- a/Content.Server/Physics/Controllers/SingularityController.cs +++ b/Content.Server/Physics/Controllers/SingularityController.cs @@ -1,3 +1,4 @@ +using System; using Content.Server.Singularity.Components; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; @@ -12,7 +13,8 @@ namespace Content.Server.Physics.Controllers { [Dependency] private readonly IRobustRandom _robustRandom = default!; - private const float MaxMoveCooldown = 10f; + // SS13 has 10s but that's quite a while + private const float MaxMoveCooldown = 5f; private const float MinMoveCooldown = 2f; public override void UpdateBeforeSolve(bool prediction, float frameTime) @@ -28,7 +30,7 @@ namespace Content.Server.Physics.Controllers if (singularity.MoveAccumulator > 0f) continue; - singularity.MoveAccumulator = MinMoveCooldown + (MaxMoveCooldown - MinMoveCooldown) * _robustRandom.NextFloat(); + singularity.MoveAccumulator = _robustRandom.NextFloat(MinMoveCooldown, MaxMoveCooldown); MoveSingulo(singularity, physics); } @@ -44,13 +46,12 @@ namespace Content.Server.Physics.Controllers } // TODO: Could try gradual changes instead - var pushVector = new Vector2(_robustRandom.Next(-10, 10), _robustRandom.Next(-10, 10)); - - if (pushVector == Vector2.Zero) return; + var pushAngle = _robustRandom.NextAngle(); + var pushStrength = _robustRandom.NextFloat(0.75f, 1.0f); physics.LinearVelocity = Vector2.Zero; physics.BodyStatus = BodyStatus.InAir; - physics.ApplyLinearImpulse(pushVector.Normalized + 1f / singularity.Level * physics.Mass); + physics.ApplyLinearImpulse(pushAngle.ToVec() * (pushStrength + 10f / Math.Min(singularity.Level, 4) * physics.Mass)); // TODO: Speedcap it probably? } } diff --git a/Content.Server/Singularity/EntitySystems/SingularitySystem.cs b/Content.Server/Singularity/EntitySystems/SingularitySystem.cs index ed128475d6..506c64be46 100644 --- a/Content.Server/Singularity/EntitySystems/SingularitySystem.cs +++ b/Content.Server/Singularity/EntitySystems/SingularitySystem.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Content.Server.Ghost.Components; using Content.Server.Singularity.Components; using Content.Shared.Singularity; @@ -14,10 +15,11 @@ using Robust.Shared.Physics.Dynamics; namespace Content.Server.Singularity.EntitySystems { [UsedImplicitly] - public class SingularitySystem : SharedSingularitySystem + public sealed class SingularitySystem : SharedSingularitySystem { [Dependency] private readonly IEntityLookup _lookup = default!; [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; /// /// How much energy the singulo gains from destroying a tile. @@ -33,11 +35,28 @@ namespace Content.Server.Singularity.EntitySystems public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(HandleCollide); + SubscribeLocalEvent(OnCollide); } - private void HandleCollide(EntityUid uid, ServerSingularityComponent component, StartCollideEvent args) + protected override bool PreventCollide(EntityUid uid, SharedSingularityComponent component, PreventCollideEvent args) { + if (base.PreventCollide(uid, component, args)) return true; + + var otherUid = args.BodyB.Owner; + + if (args.Cancelled) return true; + + // If it's not cancelled then we'll cancel if we can't immediately destroy it on collision + if (!CanDestroy(component, otherUid)) + args.Cancel(); + + return true; + } + + private void OnCollide(EntityUid uid, ServerSingularityComponent component, StartCollideEvent args) + { + if (args.OurFixture.ID != "DeleteCircle") return; + // This handles bouncing off of containment walls. // If you want the delete behavior we do it under DeleteEntities for reasons (not everything has physics). @@ -46,9 +65,10 @@ namespace Content.Server.Singularity.EntitySystems if (component.BeingDeletedByAnotherSingularity) return; - // Using this to also get smooth deletions is hard because we need to be hard for good bounce - // off of containment but also we need to be non-hard so we can freely move through the station. - // For now I've just made it so only the lookup does deletions and collision is just for fields. + var otherUid = args.OtherFixture.Body.Owner; + + // HandleDestroy will also check CanDestroy for us + HandleDestroy(component, otherUid); } public override void Update(float frameTime) @@ -71,21 +91,21 @@ namespace Content.Server.Singularity.EntitySystems { _gravityAccumulator -= GravityCooldown; - foreach (var singularity in EntityManager.EntityQuery()) + foreach (var (singularity, xform) in EntityManager.EntityQuery()) { - Update(singularity, GravityCooldown); + Update(singularity, xform, GravityCooldown); } } } - private void Update(ServerSingularityComponent component, float frameTime) + private void Update(ServerSingularityComponent component, TransformComponent xform, float frameTime) { if (component.BeingDeletedByAnotherSingularity) return; - var worldPos = EntityManager.GetComponent(component.Owner).WorldPosition; - DestroyEntities(component, worldPos); - DestroyTiles(component, worldPos); - PullEntities(component, worldPos); + var worldPos = xform.WorldPosition; + DestroyEntities(component, xform, worldPos); + DestroyTiles(component, xform, worldPos); + PullEntities(component, xform, worldPos, frameTime); } private float PullRange(ServerSingularityComponent component) @@ -101,17 +121,18 @@ namespace Content.Server.Singularity.EntitySystems private bool CanDestroy(SharedSingularityComponent component, EntityUid entity) { - return entity == component.Owner || - EntityManager.HasComponent(entity) || - EntityManager.HasComponent(entity) || - EntityManager.HasComponent(entity) || - EntityManager.HasComponent(entity); + return entity != component.Owner && + !EntityManager.HasComponent(entity) && + !EntityManager.HasComponent(entity) && + (component.Level > 4 || + !EntityManager.HasComponent(entity) && + !EntityManager.HasComponent(entity)); } private void HandleDestroy(ServerSingularityComponent component, EntityUid entity) { // TODO: Need singuloimmune tag - if (CanDestroy(component, entity)) return; + if (!CanDestroy(component, entity)) return; // Singularity priority management / etc. if (EntityManager.TryGetComponent(entity, out var otherSingulo)) @@ -125,23 +146,23 @@ namespace Content.Server.Singularity.EntitySystems otherSingulo.BeingDeletedByAnotherSingularity = true; } - EntityManager.QueueDeleteEntity(entity); - if (EntityManager.TryGetComponent(entity, out var singuloFood)) component.Energy += singuloFood.Energy; else component.Energy++; + + EntityManager.QueueDeleteEntity(entity); } /// /// Handle deleting entities and increasing energy /// - private void DestroyEntities(ServerSingularityComponent component, Vector2 worldPos) + private void DestroyEntities(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos) { // The reason we don't /just/ use collision is because we'll be deleting stuff that may not necessarily have physics (e.g. carpets). var destroyRange = DestroyTileRange(component); - foreach (var entity in _lookup.GetEntitiesInRange(EntityManager.GetComponent(component.Owner).MapID, worldPos, destroyRange)) + foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, worldPos, destroyRange)) { HandleDestroy(component, entity); } @@ -152,17 +173,21 @@ namespace Content.Server.Singularity.EntitySystems return !(EntityManager.HasComponent(entity) || EntityManager.HasComponent(entity) || EntityManager.HasComponent(entity) || - entity.IsInContainer()); + EntityManager.HasComponent(entity) || + _container.IsEntityInContainer(entity)); } - private void PullEntities(ServerSingularityComponent component, Vector2 worldPos) + /// + /// Pull dynamic bodies in range to the singulo. + /// + private void PullEntities(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos, float frameTime) { // TODO: When we split up dynamic and static trees we might be able to make items always on the broadphase // in which case we can just query dynamictree directly for brrt var pullRange = PullRange(component); var destroyRange = DestroyTileRange(component); - foreach (var entity in _lookup.GetEntitiesInRange(EntityManager.GetComponent(component.Owner).MapID, worldPos, pullRange)) + foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, worldPos, pullRange)) { // I tried having it so level 6 can de-anchor. BAD IDEA, MASSIVE LAG. if (entity == component.Owner || @@ -175,31 +200,55 @@ namespace Content.Server.Singularity.EntitySystems if (vec.Length < destroyRange - 0.01f) continue; - var speed = vec.Length * component.Level * collidableComponent.Mass; + var speed = vec.Length * component.Level * collidableComponent.Mass * 100f; // Because tile friction is so high we'll just multiply by mass so stuff like closets can even move. - collidableComponent.ApplyLinearImpulse(vec.Normalized * speed); + collidableComponent.ApplyLinearImpulse(vec.Normalized * speed * frameTime); } } /// /// Destroy any grid tiles within the relevant Level range. /// - private void DestroyTiles(ServerSingularityComponent component, Vector2 worldPos) + private void DestroyTiles(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos) { var radius = DestroyTileRange(component); var circle = new Circle(worldPos, radius); var box = new Box2(worldPos - radius, worldPos + radius); - foreach (var grid in _mapManager.FindGridsIntersecting(EntityManager.GetComponent(component.Owner).MapID, box)) + foreach (var grid in _mapManager.FindGridsIntersecting(xform.MapID, box)) { + // Bundle these together so we can use the faster helper to set tiles. + var toDestroy = new List<(Vector2i, Tile)>(); + foreach (var tile in grid.GetTilesIntersecting(circle)) { if (tile.Tile.IsEmpty) continue; - grid.SetTile(tile.GridIndices, Tile.Empty); - component.Energy += TileEnergyGain; + + // Avoid ripping up tiles that may be essential to containment + if (component.Level < 5) + { + var canDelete = true; + + foreach (var ent in grid.GetAnchoredEntities(tile.GridIndices)) + { + if (EntityManager.HasComponent(ent) || + EntityManager.HasComponent(ent)) + { + canDelete = false; + break; + } + } + + if (!canDelete) continue; + } + + toDestroy.Add((tile.GridIndices, Tile.Empty)); } + + component.Energy += TileEnergyGain * toDestroy.Count; + grid.SetTiles(toDestroy); } } } diff --git a/Content.Shared/Singularity/SharedSingularitySystem.cs b/Content.Shared/Singularity/SharedSingularitySystem.cs index 25c4c14a6e..6716db5b16 100644 --- a/Content.Shared/Singularity/SharedSingularitySystem.cs +++ b/Content.Shared/Singularity/SharedSingularitySystem.cs @@ -1,4 +1,5 @@ using System; +using Content.Shared.Ghost; using Content.Shared.Radiation; using Content.Shared.Singularity.Components; using Robust.Shared.GameObjects; @@ -46,6 +47,46 @@ namespace Content.Shared.Singularity }; } + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnPreventCollide); + } + + protected void OnPreventCollide(EntityUid uid, SharedSingularityComponent component, PreventCollideEvent args) + { + PreventCollide(uid, component, args); + } + + protected virtual bool PreventCollide(EntityUid uid, SharedSingularityComponent component, + PreventCollideEvent args) + { + var otherUid = args.BodyB.Owner; + + // For prediction reasons always want the client to ignore these. + if (EntityManager.HasComponent(otherUid) || + EntityManager.HasComponent(otherUid)) + { + args.Cancel(); + return true; + } + + // If we're above 4 then breach containment + // otherwise, check if it's containment and just keep the collision + if (EntityManager.HasComponent(otherUid) || + EntityManager.HasComponent(otherUid)) + { + if (component.Level > 4) + { + args.Cancel(); + } + + return true; + } + + return false; + } + public void ChangeSingularityLevel(SharedSingularityComponent singularity, int value) { if (value == singularity.Level) @@ -91,24 +132,5 @@ namespace Content.Shared.Singularity singularity.Dirty(); } - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(HandleFieldCollision); - } - - private void HandleFieldCollision(EntityUid uid, SharedSingularityComponent component, PreventCollideEvent args) - { - var other = args.BodyB.Owner; - - if ((!EntityManager.HasComponent(other) && - !EntityManager.HasComponent(other)) || - component.Level >= 4) - { - args.Cancel(); - return; - } - } } }