diff --git a/Content.Server/Physics/Components/RandomWalkComponent.cs b/Content.Server/Physics/Components/RandomWalkComponent.cs index e017f61ddd..90f035b84d 100644 --- a/Content.Server/Physics/Components/RandomWalkComponent.cs +++ b/Content.Server/Physics/Components/RandomWalkComponent.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Server.Physics.Controllers; namespace Content.Server.Physics.Components; @@ -29,6 +30,18 @@ public sealed partial class RandomWalkComponent : Component [ViewVariables(VVAccess.ReadWrite)] public float AccumulatorRatio = 0.0f; + /// + /// The vector by which the random walk direction is biased. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public Vector2 BiasVector = new Vector2(0f, 0f); + + /// + /// Whether to set BiasVector to (0, 0) every random walk update. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool ResetBiasOnWalk = true; + /// /// Whether this random walker should take a step immediately when it starts up. /// diff --git a/Content.Server/Physics/Controllers/RandomWalkController.cs b/Content.Server/Physics/Controllers/RandomWalkController.cs index 4a93a9e706..61d393c488 100644 --- a/Content.Server/Physics/Controllers/RandomWalkController.cs +++ b/Content.Server/Physics/Controllers/RandomWalkController.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Server.Physics.Components; using Content.Shared.Follower.Components; using Content.Shared.Throwing; @@ -69,11 +70,15 @@ internal sealed class RandomWalkController : VirtualController if(!Resolve(uid, ref physics)) return; - var pushAngle = _random.NextAngle(); + var pushVec = _random.NextAngle().ToVec(); + pushVec += randomWalk.BiasVector; + pushVec.Normalize(); + if (randomWalk.ResetBiasOnWalk) + randomWalk.BiasVector *= 0f; var pushStrength = _random.NextFloat(randomWalk.MinSpeed, randomWalk.MaxSpeed); _physics.SetLinearVelocity(uid, physics.LinearVelocity * randomWalk.AccumulatorRatio, body: physics); - _physics.ApplyLinearImpulse(uid, pushAngle.ToVec() * (pushStrength * physics.Mass), body: physics); + _physics.ApplyLinearImpulse(uid, pushVec * (pushStrength * physics.Mass), body: physics); } /// diff --git a/Content.Server/Singularity/Components/SingularityAttractorComponent.cs b/Content.Server/Singularity/Components/SingularityAttractorComponent.cs new file mode 100644 index 0000000000..d5adfd92c1 --- /dev/null +++ b/Content.Server/Singularity/Components/SingularityAttractorComponent.cs @@ -0,0 +1,30 @@ +using Content.Server.Singularity.EntitySystems; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.Singularity.Components; + +/// +/// Attracts the singularity. +/// +[RegisterComponent] +[Access(typeof(SingularityAttractorSystem))] +public sealed partial class SingularityAttractorComponent : Component +{ + /// + /// The range at which singularities will be unable to go away from the attractor. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float BaseRange = 25f; + + /// + /// The amount of time that should elapse between pulses of this attractor. + /// + [DataField, ViewVariables(VVAccess.ReadOnly)] + public TimeSpan TargetPulsePeriod = TimeSpan.FromSeconds(2); + + /// + /// The last time this attractor pulsed. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan LastPulseTime = default!; +} diff --git a/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs b/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs new file mode 100644 index 0000000000..3c44a7fc7a --- /dev/null +++ b/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs @@ -0,0 +1,106 @@ +using Content.Server.Physics.Components; +using Content.Server.Power.EntitySystems; +using Content.Server.Singularity.Components; +using Content.Shared.Singularity.Components; +using Content.Shared.Singularity.EntitySystems; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Timing; +using System.Numerics; + +namespace Content.Server.Singularity.EntitySystems; + +/// +/// Handles singularity attractors. +/// +public sealed class SingularityAttractorSystem : EntitySystem +{ + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + /// + /// The minimum range at which the attraction will act. + /// Prevents division by zero problems. + /// + public const float MinAttractRange = 0.00001f; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + } + + /// + /// Updates the pulse cooldowns of all singularity attractors. + /// If they are off cooldown it makes them emit an attraction pulse and reset their cooldown. + /// + /// The time elapsed since the last set of updates. + public override void Update(float frameTime) + { + if (!_timing.IsFirstTimePredicted) + return; + + var query = EntityQueryEnumerator(); + var now = _timing.CurTime; + while (query.MoveNext(out var uid, out var attractor, out var xform)) + { + if (attractor.LastPulseTime + attractor.TargetPulsePeriod <= now) + Update(uid, attractor, xform); + } + } + + /// + /// Makes an attractor attract all singularities and puts it on cooldown. + /// + /// The uid of the attractor to make pulse. + /// The state of the attractor to make pulse. + /// The transform of the attractor to make pulse. + private void Update(EntityUid uid, SingularityAttractorComponent? attractor = null, TransformComponent? xform = null) + { + if (!Resolve(uid, ref attractor, ref xform)) + return; + + if (!this.IsPowered(uid, EntityManager)) + return; + + attractor.LastPulseTime = _timing.CurTime; + + var mapPos = xform.Coordinates.ToMap(EntityManager); + + if (mapPos == MapCoordinates.Nullspace) + return; + + var query = EntityQuery(); + foreach (var (singulo, walk, singuloXform) in query) + { + var singuloMapPos = singuloXform.Coordinates.ToMap(EntityManager); + + if (singuloMapPos.MapId != mapPos.MapId) + continue; + + var biasBy = mapPos.Position - singuloMapPos.Position; + var length = biasBy.Length(); + if (length <= MinAttractRange) + return; + + biasBy = Vector2.Normalize(biasBy) * (attractor.BaseRange / length); + + walk.BiasVector += biasBy; + } + } + + /// + /// Resets the pulse timings of the attractor when the component starts up. + /// + /// The uid of the attractor to start up. + /// The state of the attractor to start up. + /// The startup prompt arguments. + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.LastPulseTime = _timing.CurTime; + } +} diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index 50823a5897..746ae3d976 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -142,6 +142,9 @@ uplink-voice-mask-desc = A gas mask that lets you adjust your voice to whoever y uplink-radio-jammer-name = Radio Jammer uplink-radio-jammer-desc = This device will disrupt any nearby outgoing radio communication when activated. +uplink-singularity-beacon-name = Singularity Beacon +uplink-singularity-beacon-desc = A device that attracts singularities. Has to be anchored and powered. Causes singularities to grow when consumed. + # Implants uplink-storage-implanter-name = Storage Implanter uplink-storage-implanter-desc = Hide goodies inside of yourself with new bluespace technology! diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index 36c3c1435f..aa6bf74096 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -1123,6 +1123,16 @@ - ResearchDirector - Chef +- type: listing + id: UplinkSingarityBeacon + name: uplink-singularity-beacon-name + description: uplink-singularity-beacon-desc + productEntity: SingularityBeacon + cost: + Telecrystal: 12 + categories: + - UplinkUtility + # Armor - type: listing diff --git a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/singularity_beacon.yml b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/singularity_beacon.yml new file mode 100644 index 0000000000..372b689113 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/singularity_beacon.yml @@ -0,0 +1,40 @@ +- type: entity + id: SingularityBeacon + parent: BaseMachinePowered + name: singularity beacon + description: A syndicate device that attracts the singularity. If it's loose and you're seeing this, run. + components: + - type: Sprite + sprite: Objects/Devices/singularity_beacon.rsi + layers: + - state: icon + - type: Item + size: Huge + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.40,-0.40,0.40,0.40" + density: 80 + mask: + - MachineMask + layer: + - MachineLayer + - type: SingularityAttractor + baseRange: 80 + - type: SinguloFood + energy: 120 + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 50 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: ApcPowerReceiver + powerLoad: 15000 + priority: High + - type: StaticPrice + price: 1500 diff --git a/Resources/Textures/Objects/Devices/singularity_beacon.rsi/icon.png b/Resources/Textures/Objects/Devices/singularity_beacon.rsi/icon.png new file mode 100644 index 0000000000..78bb95b874 Binary files /dev/null and b/Resources/Textures/Objects/Devices/singularity_beacon.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Devices/singularity_beacon.rsi/meta.json b/Resources/Textures/Objects/Devices/singularity_beacon.rsi/meta.json new file mode 100644 index 0000000000..2347b239a1 --- /dev/null +++ b/Resources/Textures/Objects/Devices/singularity_beacon.rsi/meta.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "tgstation at https://github.com/tgstation/tgstation/blob/c46d689e2b300bdc8c5b153855b1a5bbb2c5b168/icons/obj/singularity.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon", + "delays": [ + [ + 0.1, + 0.1 + ] + ] + } + ] +}