diff --git a/Content.Client/_Impstation/Gravity/IsInZeroGravityAreaComponent.cs b/Content.Client/_Impstation/Gravity/IsInZeroGravityAreaComponent.cs new file mode 100644 index 000000000000..36120b0171ce --- /dev/null +++ b/Content.Client/_Impstation/Gravity/IsInZeroGravityAreaComponent.cs @@ -0,0 +1,28 @@ +using Robust.Shared.GameStates; + +namespace Content.Client.Gravity; + +[RegisterComponent, NetworkedComponent] +public sealed partial class IsInZeroGravityAreaComponent : Component +{ + /// + /// When this number is non-zero, the entity is most likely weightless. + /// + /// + /// This is a bitmask of all the entity IDs of the affecting areas bitwise-OR'd together. + /// Prediction calculation of movement needs to be blistering fast to remain seamless, so I + /// am utilizing some bitwise tricks to try and estimate whether or not the player is being + /// affected by a ZeroGravityArea. + /// + /// When a player enters a ZeroGravityArea, the area's NetEntity ID will be bitwise-OR'd into + /// the fingerprint. + /// When a player leaves a ZeroGravityArea, the fingerprint will be bitwise-AND'ed with the + /// negation of the area's NetEntity ID. + /// This leaves us with an extremely rough approximate idea of whether or not we are still + /// being affected by an area. Theoretically, it should guarantee correctness in the case + /// of two overlapping ZeroGravityAreas, and decently high chance of correctness with + /// three overlapping areas. + /// If there's more, fuck it. I tried. + [DataField, ViewVariables(VVAccess.ReadOnly)] + public int AreaFingerprint = 0; +} diff --git a/Content.Client/_Impstation/Gravity/ZeroGravityAreaComponent.cs b/Content.Client/_Impstation/Gravity/ZeroGravityAreaComponent.cs new file mode 100644 index 000000000000..829df3259b88 --- /dev/null +++ b/Content.Client/_Impstation/Gravity/ZeroGravityAreaComponent.cs @@ -0,0 +1,11 @@ +using Content.Client.Gravity; +using Content.Shared._Impstation.Gravity; +using Robust.Shared.GameStates; + +namespace Content.Client._Impstation.Gravity; + +[RegisterComponent, NetworkedComponent] +[Access(typeof(ZeroGravityAreaSystem))] +public sealed partial class ZeroGravityAreaComponent : SharedZeroGravityAreaComponent +{ +} diff --git a/Content.Client/_Impstation/Gravity/ZeroGravityAreaSystem.cs b/Content.Client/_Impstation/Gravity/ZeroGravityAreaSystem.cs new file mode 100644 index 000000000000..4c52c8ddd8ff --- /dev/null +++ b/Content.Client/_Impstation/Gravity/ZeroGravityAreaSystem.cs @@ -0,0 +1,93 @@ +using System.Linq; +using Content.Client._Impstation.Gravity; +using Content.Shared._Impstation.Gravity; +using Content.Shared.Clothing; +using Content.Shared.Gravity; +using Robust.Client.Physics; +using Robust.Shared.GameStates; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; + +namespace Content.Client.Gravity; + +public sealed partial class ZeroGravityAreaSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnCheckWeightless, after: [typeof(SharedMagbootsSystem)]); + SubscribeLocalEvent(OnHandleEntityState); + SubscribeLocalEvent(OnHandleAreaState); + SubscribeLocalEvent(OnStartCollide); + SubscribeLocalEvent(OnEndCollide); + } + + public bool IsEnabled(EntityUid uid, ZeroGravityAreaComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return false; + + return comp.Enabled; + } + + private void OnHandleEntityState(EntityUid uid, IsInZeroGravityAreaComponent comp, ComponentHandleState args) + { + if (args.Current is not IsInZeroGravityAreaState state) + return; + + comp.AreaFingerprint = state.AreaFingerprint; + } + + private void OnHandleAreaState(EntityUid uid, ZeroGravityAreaComponent comp, ComponentHandleState args) + { + if (args.Current is not ZeroGravityAreaState state) + return; + + comp.Enabled = state.Enabled; + } + + private void OnStartCollide(EntityUid uid, ZeroGravityAreaComponent comp, StartCollideEvent args) + { + var other = args.OtherEntity; + + if (args.OurFixtureId != comp.Fixture) + return; + + if (!TryComp(other, out var physics)) + return; + + if (!physics.Predict || (physics.BodyType & (BodyType.Static | BodyType.Kinematic)) != 0) + return; + + Log.Debug($"Predicting that {args.OtherEntity} enters anti-grav area {uid}"); + + var antiGrav = EnsureComp(other); + antiGrav.AreaFingerprint |= other.Id; + Dirty(other, antiGrav); + } + + private void OnEndCollide(EntityUid uid, ZeroGravityAreaComponent comp, EndCollideEvent args) + { + var other = args.OtherEntity; + + if (args.OurFixtureId != comp.Fixture) + return; + + if (!TryComp(other, out var antiGrav)) + return; + + antiGrav.AreaFingerprint &= ~GetNetEntity(uid).Id; + Dirty(other, antiGrav); + } + + private void OnCheckWeightless(EntityUid uid, IsInZeroGravityAreaComponent comp, ref IsWeightlessEvent args) + { + if (args.Handled) + return; + + args.IsWeightless = comp.AreaFingerprint != 0; + args.Handled = args.IsWeightless; + } +} diff --git a/Content.Server/_Impstation/Gravity/IsInZeroGravityAreaComponent.cs b/Content.Server/_Impstation/Gravity/IsInZeroGravityAreaComponent.cs new file mode 100644 index 000000000000..d27af37a759d --- /dev/null +++ b/Content.Server/_Impstation/Gravity/IsInZeroGravityAreaComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Server._Impstation.Gravity; + +[RegisterComponent, NetworkedComponent] +[Access(typeof(ZeroGravityAreaSystem))] +public sealed partial class IsInZeroGravityAreaComponent : Component +{ + [DataField, ViewVariables(VVAccess.ReadOnly)] + public HashSet> AffectingAreas = new(); +} diff --git a/Content.Server/_Impstation/Gravity/ZeroGravityAreaComponent.cs b/Content.Server/_Impstation/Gravity/ZeroGravityAreaComponent.cs new file mode 100644 index 000000000000..6fab3b1ceee5 --- /dev/null +++ b/Content.Server/_Impstation/Gravity/ZeroGravityAreaComponent.cs @@ -0,0 +1,11 @@ +using Content.Shared._Impstation.Gravity; + +namespace Content.Server._Impstation.Gravity; + +[RegisterComponent] +[Access(typeof(ZeroGravityAreaSystem))] +public sealed partial class ZeroGravityAreaComponent : SharedZeroGravityAreaComponent +{ + [DataField, ViewVariables(VVAccess.ReadOnly)] + public HashSet> AffectedEntities = new(); +} diff --git a/Content.Server/_Impstation/Gravity/ZeroGravityAreaSystem.cs b/Content.Server/_Impstation/Gravity/ZeroGravityAreaSystem.cs new file mode 100644 index 000000000000..924dd309defe --- /dev/null +++ b/Content.Server/_Impstation/Gravity/ZeroGravityAreaSystem.cs @@ -0,0 +1,129 @@ +using System.Linq; +using Content.Shared._Impstation.Gravity; +using Content.Shared.Clothing; +using Content.Shared.Gravity; +using Robust.Shared.GameStates; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; + +namespace Content.Server._Impstation.Gravity; + +public sealed partial class ZeroGravityAreaSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartCollision); + SubscribeLocalEvent(OnEndCollision); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnGetAreaState); + + SubscribeLocalEvent(OnCheckWeightless, after: [typeof(SharedMagbootsSystem)]); + SubscribeLocalEvent(OnGetEntityState); + } + + public bool IsEnabled(EntityUid uid, ZeroGravityAreaComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return false; + + return comp.Enabled; + } + + public void SetEnabled(EntityUid uid, bool enabled, ZeroGravityAreaComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return; + + comp.Enabled = enabled; + Dirty(uid, comp); + foreach (var ent in comp.AffectedEntities) + { + // Update entity states to see if they're no longer weightless + Dirty(ent); + } + } + + private void StartAffecting(Entity area, Entity entity) + { + area.Comp.AffectedEntities.Add(entity); + entity.Comp.AffectingAreas.Add(area); + Dirty(entity); + } + + private void StopAffecting(Entity area, Entity entity) + { + area.Comp.AffectedEntities.Remove(entity); + entity.Comp.AffectingAreas.Remove(area); + Dirty(entity); + } + + private void OnStartCollision(EntityUid uid, ZeroGravityAreaComponent comp, StartCollideEvent args) + { + if (args.OurFixtureId != comp.Fixture) + return; + + if (!TryComp(args.OtherEntity, out var physics)) + return; + + if ((physics.BodyType & (BodyType.Kinematic | BodyType.Static)) != 0) + return; + + var antiGrav = EnsureComp(args.OtherEntity); + StartAffecting((uid, comp), (args.OtherEntity, antiGrav)); + } + + private void OnEndCollision(EntityUid uid, ZeroGravityAreaComponent comp, EndCollideEvent args) + { + if (args.OurFixtureId != comp.Fixture) + return; + + if (!TryComp(args.OtherEntity, out var antiGrav)) + return; + + StopAffecting((uid, comp), (args.OtherEntity, antiGrav)); + } + + private void OnShutdown(EntityUid uid, ZeroGravityAreaComponent comp, ComponentShutdown args) + { + foreach (var ent in comp.AffectedEntities) + { + ent.Comp.AffectingAreas.Remove((uid, comp)); + Dirty(ent); + } + } + + private void OnGetAreaState(Entity ent, ref ComponentGetState args) + { + args.State = new ZeroGravityAreaState(ent.Comp); + } + + private bool EntityIsWeightless(IsInZeroGravityAreaComponent ent) + { + return ent.AffectingAreas.Any(area => area.Comp.Enabled); + } + + private void OnCheckWeightless(EntityUid uid, IsInZeroGravityAreaComponent comp, ref IsWeightlessEvent args) + { + if (args.Handled) + return; + + if (EntityIsWeightless(comp)) + { + args.IsWeightless = true; + args.Handled = true; + } + } + + private void OnGetEntityState(EntityUid uid, IsInZeroGravityAreaComponent comp, ref ComponentGetState args) + { + args.State = new IsInZeroGravityAreaState(comp.AffectingAreas.Aggregate(0, (fingerprint, area) => + { + if (area.Comp.Enabled) + fingerprint |= GetNetEntity(area.Owner).Id; + return fingerprint; + })); + } +} diff --git a/Content.Shared/_Impstation/Gravity/IsInZeroGravityAreaState.cs b/Content.Shared/_Impstation/Gravity/IsInZeroGravityAreaState.cs new file mode 100644 index 000000000000..640d2a5319d4 --- /dev/null +++ b/Content.Shared/_Impstation/Gravity/IsInZeroGravityAreaState.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Gravity; + +[Serializable, NetSerializable] +public sealed partial class IsInZeroGravityAreaState : ComponentState +{ + public IsInZeroGravityAreaState(int areaFingerprint) + { + AreaFingerprint = areaFingerprint; + } + public int AreaFingerprint; +} diff --git a/Content.Shared/_Impstation/Gravity/ZeroGravityAreaComponent.cs b/Content.Shared/_Impstation/Gravity/ZeroGravityAreaComponent.cs new file mode 100644 index 000000000000..7756362b0b4d --- /dev/null +++ b/Content.Shared/_Impstation/Gravity/ZeroGravityAreaComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared._Impstation.Gravity; +public abstract partial class SharedZeroGravityAreaComponent : Component +{ + [DataField(readOnly: true), ViewVariables(VVAccess.ReadWrite)] + public string Fixture = "antiGravity"; + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool Enabled = true; +} + +[Serializable, NetSerializable] +public sealed partial class ZeroGravityAreaState : ComponentState +{ + public ZeroGravityAreaState(SharedZeroGravityAreaComponent comp) + { + Enabled = comp.Enabled; + } + + public bool Enabled; +} diff --git a/Resources/Prototypes/_Impstation/CosmicCult/Objects/structures.yml b/Resources/Prototypes/_Impstation/CosmicCult/Objects/structures.yml new file mode 100644 index 000000000000..01b4b510e1f5 --- /dev/null +++ b/Resources/Prototypes/_Impstation/CosmicCult/Objects/structures.yml @@ -0,0 +1,53 @@ +- type: entity + name: anti-gravity pylon + id: AntiGravityPylon + parent: BaseMachine + description: "A mysterious construct that makes the area around it weightless..." + components: + - type: ZeroGravityArea + fixture: antiGravity + - type: Sprite + sprite: Objects/Misc/Lights/lights.rsi + layers: + - state: floodlight + - state: floodlight_on + shader: unshaded + visible: false + map: [ "light" ] + - type: Physics + canCollide: true + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.2, -0.5, 0.2, 0.5" + density: 50 + mask: + - HighImpassable + antiGravity: + shape: + !type:PhysShapeCircle + radius: 3 + hard: false + layer: + - Impassable + - type: PointLight + radius: 4 + energy: 5 + color: "#7700FFFF" + mask: /Textures/Effects/LightMasks/double_cone.png + - type: RotatingLight + speed: 100 + - type: Anchorable + - type: Damageable + damageContainer: StructuralInorganic + damageModifierSet: Metallic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 100 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ]