diff --git a/Content.Client/Singularity/Systems/EventHorizonSystem.cs b/Content.Client/Singularity/Systems/EventHorizonSystem.cs index 3dd63a0c9c889e..606116fced45af 100644 --- a/Content.Client/Singularity/Systems/EventHorizonSystem.cs +++ b/Content.Client/Singularity/Systems/EventHorizonSystem.cs @@ -1,7 +1,7 @@ using Content.Shared.Singularity.EntitySystems; using Content.Shared.Singularity.Components; -namespace Content.Client.Singularity.EntitySystems; +namespace Content.Client.Singularity.Systems; /// /// The client-side version of . diff --git a/Content.Client/Singularity/Systems/SingularityGeneratorSystem.cs b/Content.Client/Singularity/Systems/SingularityGeneratorSystem.cs new file mode 100644 index 00000000000000..f5b85f8b3cfead --- /dev/null +++ b/Content.Client/Singularity/Systems/SingularityGeneratorSystem.cs @@ -0,0 +1,12 @@ +using Content.Shared.Singularity.EntitySystems; +using Content.Shared.Singularity.Components; + +namespace Content.Client.Singularity.Systems; + +/// +/// The client-side version of . +/// Manages s. +/// Exists to make relevant signal handlers (ie: ) work on the client. +/// +public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSystem +{} diff --git a/Content.Client/Singularity/Systems/SingularitySystem.cs b/Content.Client/Singularity/Systems/SingularitySystem.cs index 5293ad499d07b8..50a12466be1397 100644 --- a/Content.Client/Singularity/Systems/SingularitySystem.cs +++ b/Content.Client/Singularity/Systems/SingularitySystem.cs @@ -5,7 +5,7 @@ using Robust.Shared.GameStates; using Robust.Shared.Utility; -namespace Content.Client.Singularity.EntitySystems; +namespace Content.Client.Singularity.Systems; /// /// The client-side version of . diff --git a/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs b/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs deleted file mode 100644 index ea2628e5cb8286..00000000000000 --- a/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -using Content.Server.Singularity.EntitySystems; - -namespace Content.Server.Singularity.Components; - -[RegisterComponent] -public sealed partial class SingularityGeneratorComponent : Component -{ - /// - /// The amount of power this generator has accumulated. - /// If you want to set this use - /// - [DataField("power")] - [Access(friends:typeof(SingularityGeneratorSystem))] - public float Power = 0; - - /// - /// The power threshold at which this generator will spawn a singularity. - /// If you want to set this use - /// - [DataField("threshold")] - [Access(friends:typeof(SingularityGeneratorSystem))] - public float Threshold = 16; - - /// - /// The prototype ID used to spawn a singularity. - /// - [DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer))] - [ViewVariables(VVAccess.ReadWrite)] - public string? SpawnPrototype = "Singularity"; -} diff --git a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs index a0c0262794824b..95722449b87799 100644 --- a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs @@ -1,14 +1,23 @@ using Content.Server.ParticleAccelerator.Components; -using Content.Server.Singularity.Components; +using Content.Shared.Popups; using Content.Shared.Singularity.Components; +using Content.Shared.Singularity.EntitySystems; +using Robust.Server.GameObjects; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; +using Robust.Shared.Timing; namespace Content.Server.Singularity.EntitySystems; -public sealed class SingularityGeneratorSystem : EntitySystem +public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSystem { #region Dependencies [Dependency] private readonly IViewVariablesManager _vvm = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly PhysicsSystem _physics = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly MetaDataSystem _metadata = default!; #endregion Dependencies public override void Initialize() @@ -100,11 +109,37 @@ public void SetThreshold(EntityUid uid, float value, SingularityGeneratorCompone /// The state of the beginning of the collision. private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent component, ref StartCollideEvent args) { - if (EntityManager.TryGetComponent(args.OtherEntity, out var singularityGeneratorComponent)) + if (!EntityManager.TryGetComponent(args.OtherEntity, out var generatorComp)) + return; + + if (_timing.CurTime < _metadata.GetPauseTime(uid) + generatorComp.NextFailsafe && !generatorComp.FailsafeDisabled) + { + EntityManager.QueueDeleteEntity(uid); + return; + } + + var contained = true; + if (!generatorComp.FailsafeDisabled) + { + var transform = Transform(args.OtherEntity); + var directions = Enum.GetValues().Length; + for (var i = 0; i < directions - 1; i += 2) // Skip every other direction, checking only cardinals + { + if (!CheckContainmentField((Direction)i, new Entity(args.OtherEntity, generatorComp), transform)) + contained = false; + } + } + + if (!contained && !generatorComp.FailsafeDisabled) + { + generatorComp.NextFailsafe = _timing.CurTime + generatorComp.FailsafeCooldown; + PopupSystem.PopupEntity(Loc.GetString("comp-generator-failsafe", ("target", args.OtherEntity)), args.OtherEntity, PopupType.LargeCaution); + } + else { SetPower( args.OtherEntity, - singularityGeneratorComponent.Power + component.State switch + generatorComp.Power + component.State switch { ParticleAcceleratorPowerState.Standby => 0, ParticleAcceleratorPowerState.Level0 => 1, @@ -113,10 +148,46 @@ private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent co ParticleAcceleratorPowerState.Level3 => 8, _ => 0 }, - singularityGeneratorComponent + generatorComp ); - EntityManager.QueueDeleteEntity(uid); } + + EntityManager.QueueDeleteEntity(uid); } #endregion Event Handlers + + /// + /// Checks whether there's a containment field in a given direction away from the generator + /// + /// The transform component of the singularity generator. + /// Mostly copied from + private bool CheckContainmentField(Direction dir, Entity generator, TransformComponent transform) + { + var component = generator.Comp; + + var (worldPosition, worldRotation) = _transformSystem.GetWorldPositionRotation(transform); + var dirRad = dir.ToAngle() + worldRotation; + + var ray = new CollisionRay(worldPosition, dirRad.ToVec(), component.CollisionMask); + var rayCastResults = _physics.IntersectRay(transform.MapID, ray, component.FailsafeDistance, generator, false); + var genQuery = GetEntityQuery(); + + RayCastResults? closestResult = null; + + foreach (var result in rayCastResults) + { + if (genQuery.HasComponent(result.HitEntity)) + closestResult = result; + + break; + } + + if (closestResult == null) + return false; + + var ent = closestResult.Value.HitEntity; + + // Check that the field can't be moved. The fields' transform parenting is weird, so skip that + return TryComp(ent, out var collidableComponent) && collidableComponent.BodyType == BodyType.Static; + } } diff --git a/Content.Shared/Emag/Systems/EmagSystem.cs b/Content.Shared/Emag/Systems/EmagSystem.cs index 4d3bbcbb8e9d84..3a556b47063b75 100644 --- a/Content.Shared/Emag/Systems/EmagSystem.cs +++ b/Content.Shared/Emag/Systems/EmagSystem.cs @@ -96,6 +96,13 @@ public bool DoEmagEffect(EntityUid user, EntityUid target) } } +/// +/// Shows a popup to emag user (client side only!) and adds to the entity when handled +/// +/// Emag user +/// Did the emagging succeed? Causes a user-only popup to show on client side +/// Can the entity be emagged more than once? Prevents adding of +/// Needs to be handled in shared/client, not just the server, to actually show the emagging popup [ByRefEvent] public record struct GotEmaggedEvent(EntityUid UserUid, bool Handled = false, bool Repeatable = false); diff --git a/Content.Shared/Singularity/Components/SingularityGeneratorComponent.cs b/Content.Shared/Singularity/Components/SingularityGeneratorComponent.cs new file mode 100644 index 00000000000000..715584b5bc64d7 --- /dev/null +++ b/Content.Shared/Singularity/Components/SingularityGeneratorComponent.cs @@ -0,0 +1,69 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +using Content.Shared.Physics; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.GameStates; + +namespace Content.Shared.Singularity.Components; + +[RegisterComponent, AutoGenerateComponentPause, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SingularityGeneratorComponent : Component +{ + /// + /// The amount of power this generator has accumulated. + /// If you want to set this use + /// + [DataField] + public float Power = 0; + + /// + /// The power threshold at which this generator will spawn a singularity. + /// If you want to set this use + /// + [DataField] + public float Threshold = 16; + + /// + /// Allows the generator to ignore all the failsafe stuff, e.g. when emagged + /// + [DataField, AutoNetworkedField] + public bool FailsafeDisabled = false; + + /// + /// Maximum distance at which the generator will check for a field at + /// + [DataField] + public float FailsafeDistance = 16; + + /// + /// The prototype ID used to spawn a singularity. + /// + [DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string? SpawnPrototype = "Singularity"; + + /// + /// The masks the raycast should not go through + /// + [DataField] + public int CollisionMask = (int)CollisionGroup.FullTileMask; + + /// + /// Message to use when there's no containment field on cardinal directions + /// + [DataField] + public LocId ContainmentFailsafeMessage = "comp-generator-failsafe"; + + /// + /// For how long the failsafe will cause the generator to stop working and not issue a failsafe warning + /// + [DataField] + public TimeSpan FailsafeCooldown = TimeSpan.FromSeconds(10); + + /// + /// How long until the generator can issue a failsafe warning again + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextFailsafe = TimeSpan.Zero; +} diff --git a/Content.Shared/Singularity/EntitySystems/SharedSingularityGeneratorSystem.cs b/Content.Shared/Singularity/EntitySystems/SharedSingularityGeneratorSystem.cs new file mode 100644 index 00000000000000..ee6dc89bb8478b --- /dev/null +++ b/Content.Shared/Singularity/EntitySystems/SharedSingularityGeneratorSystem.cs @@ -0,0 +1,28 @@ +using Content.Shared.Emag.Systems; +using Content.Shared.Popups; +using Content.Shared.Singularity.Components; + +namespace Content.Shared.Singularity.EntitySystems; + +/// +/// Shared part of SingularitySingularityGeneratorSystem +/// +public abstract class SharedSingularityGeneratorSystem : EntitySystem +{ + #region Dependencies + [Dependency] protected readonly SharedPopupSystem PopupSystem = default!; + #endregion Dependencies + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEmagged); + } + + private void OnEmagged(EntityUid uid, SingularityGeneratorComponent component, ref GotEmaggedEvent args) + { + component.FailsafeDisabled = true; + args.Handled = true; + } +} \ No newline at end of file diff --git a/Resources/Locale/en-US/singularity/components/generator-component.ftl b/Resources/Locale/en-US/singularity/components/generator-component.ftl new file mode 100644 index 00000000000000..d2a04f9cbc098c --- /dev/null +++ b/Resources/Locale/en-US/singularity/components/generator-component.ftl @@ -0,0 +1,2 @@ +comp-generator-failsafe = The {$target} shakes as the containment failsafe triggers! +comp-generator-failsafe-disabled = Something fizzles out inside of {$target}... \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/generator.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/generator.yml index 647eae27724774..45a40bf0faf315 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/generator.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/generator.yml @@ -1,7 +1,7 @@ - type: entity id: SingularityGenerator name: gravitational singularity generator - description: An Odd Device which produces a Gravitational Singularity when set up. + description: An Odd Device which produces a Gravitational Singularity when set up. Comes with a temporary shutdown containment failsafe. placement: mode: SnapgridCenter components: diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/Tesla/generator.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/Tesla/generator.yml index d45e6c58ea753a..bdd90f2f16afba 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/Tesla/generator.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/Tesla/generator.yml @@ -2,12 +2,12 @@ id: TeslaGenerator name: tesla generator parent: BaseStructureDynamic - description: An Odd Device which produces a powerful Tesla ball when set up. + description: An Odd Device which produces a powerful Tesla ball when set up. Comes with a temporary shutdown containment failsafe. components: - type: Sprite noRot: true sprite: Structures/Power/Generation/Tesla/generator.rsi - state: icon + state: icon - type: SingularityGenerator # TODO: rename the generator spawnId: TeslaEnergyBall - type: InteractionOutline