diff --git a/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs b/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs index ea2628e5cb..180b849958 100644 --- a/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs +++ b/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs @@ -2,32 +2,69 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Content.Server.Singularity.EntitySystems; +using Content.Shared.Physics; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Server.Singularity.Components; -[RegisterComponent] +[RegisterComponent, AutoGenerateComponentPause] +[Access(typeof(SingularityGeneratorSystem))] 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))] + [DataField] 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))] + [DataField] public float Threshold = 16; + /// + /// Allows the generator to ignore all the failsafe stuff, e.g. when emagged + /// + [DataField] + 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))] - [ViewVariables(VVAccess.ReadWrite)] 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; + + /// + /// For how long the failsafe will cause the generator to stop working and not issue a failsafe warning + /// + [DataField] + public TimeSpan FailsafeCooldown = TimeSpan.FromSeconds(30); + + /// + /// How long until the generator can issue a failsafe warning again + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextFailsafe; } diff --git a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs index a0c0262794..be0c5e49b5 100644 --- a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs @@ -1,7 +1,15 @@ +using System.Diagnostics; using Content.Server.ParticleAccelerator.Components; +using Content.Server.Popups; using Content.Server.Singularity.Components; +using Content.Shared.Emag.Systems; +using Content.Shared.Popups; using Content.Shared.Singularity.Components; +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; @@ -9,6 +17,11 @@ public sealed class SingularityGeneratorSystem : EntitySystem { #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!; + [Dependency] private readonly PopupSystem _popupSystem = default!; #endregion Dependencies public override void Initialize() @@ -16,6 +29,7 @@ public sealed class SingularityGeneratorSystem : EntitySystem base.Initialize(); SubscribeLocalEvent(HandleParticleCollide); + SubscribeLocalEvent(OnEmagged); var vvHandle = _vvm.GetTypeHandler(); vvHandle.AddPath(nameof(SingularityGeneratorComponent.Power), (_, comp) => comp.Power, SetPower); @@ -100,11 +114,33 @@ public sealed class SingularityGeneratorSystem : EntitySystem /// 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) { + EntityManager.QueueDeleteEntity(uid); + return; + } + + var contained = true; + 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.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 +149,51 @@ public sealed class SingularityGeneratorSystem : EntitySystem ParticleAcceleratorPowerState.Level3 => 8, _ => 0 }, - singularityGeneratorComponent + generatorComp ); - EntityManager.QueueDeleteEntity(uid); - } + EntityManager.QueueDeleteEntity(uid); + } + + private void OnEmagged(EntityUid uid, SingularityGeneratorComponent component, ref GotEmaggedEvent args) + { + _popupSystem.PopupEntity(Loc.GetString("comp-generator-failsafe-disabled", ("target", uid)), uid); + component.FailsafeDisabled = true; + args.Handled = true; } #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/Resources/Locale/en-US/singularity/components/generator-component.ftl b/Resources/Locale/en-US/singularity/components/generator-component.ftl new file mode 100644 index 0000000000..f3a2254c38 --- /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 = 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 647eae2772..45a40bf0fa 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 d45e6c58ea..bdd90f2f16 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