diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 3492ce70ec..46dd76003d 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -6,6 +6,7 @@ using Content.Client.Chat.Managers; using Content.Client.EscapeMenu; using Content.Client.Eui; using Content.Client.Flash; +using Content.Client.GhostKick; using Content.Client.HUD; using Content.Client.Info; using Content.Client.Input; @@ -127,6 +128,7 @@ namespace Content.Client.Entry IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); IoCManager.InjectDependencies(this); diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index 8e9f26105a..4d7489b383 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -321,6 +321,8 @@ namespace Content.Client.Entry "Armor", "AtmosMonitor", "AtmosAlarmable", + "LandMine", + "GhostKickUserOnTrigger", "FireAlarm", "AirAlarm", "RadarConsole", diff --git a/Content.Client/GhostKick/GhostKickManager.cs b/Content.Client/GhostKick/GhostKickManager.cs new file mode 100644 index 0000000000..7ca2385e4b --- /dev/null +++ b/Content.Client/GhostKick/GhostKickManager.cs @@ -0,0 +1,40 @@ +using Content.Shared.GhostKick; +using Robust.Client; +using Robust.Shared; +using Robust.Shared.Configuration; +using Robust.Shared.Network; + +namespace Content.Client.GhostKick; + +public sealed class GhostKickManager +{ + private bool _fakeLossEnabled; + + [Dependency] private readonly IBaseClient _baseClient = default!; + [Dependency] private readonly IClientNetManager _netManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + public void Initialize() + { + _netManager.RegisterNetMessage(RxCallback); + + _baseClient.RunLevelChanged += BaseClientOnRunLevelChanged; + } + + private void BaseClientOnRunLevelChanged(object? sender, RunLevelChangedEventArgs e) + { + if (_fakeLossEnabled && e.OldLevel == ClientRunLevel.InGame) + { + _cfg.SetCVar(CVars.NetFakeLoss, 0); + + _fakeLossEnabled = false; + } + } + + private void RxCallback(MsgGhostKick message) + { + _fakeLossEnabled = true; + + _cfg.SetCVar(CVars.NetFakeLoss, 1); + } +} diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index 736da75161..c7f1e95627 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -4,6 +4,7 @@ using Content.Client.Chat.Managers; using Content.Client.Clickable; using Content.Client.EscapeMenu; using Content.Client.Eui; +using Content.Client.GhostKick; using Content.Client.HUD; using Content.Client.Info; using Content.Client.Items.Managers; @@ -43,6 +44,7 @@ namespace Content.Client.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs index f92bd879c5..0164a251ef 100644 --- a/Content.Client/Popups/PopupSystem.cs +++ b/Content.Client/Popups/PopupSystem.cs @@ -10,7 +10,6 @@ using Robust.Client.UserInterface.Controls; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Timing; -using Robust.Shared.Utility; namespace Content.Client.Popups { @@ -19,6 +18,7 @@ namespace Content.Client.Popups [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly IMapManager _map = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly ExamineSystemShared _examineSystem = default!; @@ -55,7 +55,13 @@ namespace Content.Client.Popups PopupMessage(message, _eyeManager.CoordinatesToScreen(transform.Coordinates), uid); } - public void PopupMessage(string message, ScreenCoordinates coordinates, EntityUid? entity = null) + private void PopupMessage(string message, ScreenCoordinates coordinates, EntityUid? entity = null) + { + var mapCoords = _eyeManager.ScreenToMap(coordinates); + PopupMessage(message, EntityCoordinates.FromMap(_map, mapCoords), entity); + } + + private void PopupMessage(string message, EntityCoordinates coordinates, EntityUid? entity = null) { var label = new PopupLabel(_eyeManager, EntityManager) { @@ -67,8 +73,7 @@ namespace Content.Client.Popups _userInterfaceManager.PopupRoot.AddChild(label); label.Measure(Vector2.Infinity); - var mapCoordinates = _eyeManager.ScreenToMap(coordinates.Position); - label.InitialPos = mapCoordinates; + label.InitialPos = coordinates; LayoutContainer.SetPosition(label, label.InitialPos.Position); _aliveLabels.Add(label); } @@ -161,7 +166,7 @@ namespace Content.Client.Popups continue; } - var otherPos = label.Entity != null ? Transform(label.Entity.Value).MapPosition : label.InitialPos; + var otherPos = label.Entity != null ? Transform(label.Entity.Value).MapPosition : label.InitialPos.ToMap(EntityManager); if (occluded && !ExamineSystemShared.InRangeUnOccluded( playerPos, @@ -188,7 +193,7 @@ namespace Content.Client.Popups /// /// Yes that's right it's not technically MapCoordinates. /// - public MapCoordinates InitialPos { get; set; } + public EntityCoordinates InitialPos { get; set; } public EntityUid? Entity { get; set; } public PopupLabel(IEyeManager eyeManager, IEntityManager entityManager) @@ -204,11 +209,12 @@ namespace Content.Client.Popups { TotalTime += eventArgs.DeltaSeconds; - Vector2 position; + ScreenCoordinates screenCoords; + if (Entity == null) - position = _eyeManager.WorldToScreen(InitialPos.Position) / UIScale - DesiredSize / 2; + screenCoords = _eyeManager.CoordinatesToScreen(InitialPos); else if (_entityManager.TryGetComponent(Entity.Value, out TransformComponent xform)) - position = (_eyeManager.CoordinatesToScreen(xform.Coordinates).Position / UIScale) - DesiredSize / 2; + screenCoords = _eyeManager.CoordinatesToScreen(xform.Coordinates); else { // Entity has probably been deleted. @@ -217,6 +223,7 @@ namespace Content.Client.Popups return; } + var position = screenCoords.Position / UIScale - DesiredSize / 2; LayoutContainer.SetPosition(this, position - (0, 20 * (TotalTime * TotalTime + TotalTime))); if (TotalTime > 0.5f) diff --git a/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs b/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs index 1e5f98c4c6..cb469ad0a4 100644 --- a/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs +++ b/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs @@ -4,6 +4,7 @@ using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; using Content.Shared.Slippery; +using Content.Shared.StepTrigger; using JetBrains.Annotations; using Robust.Shared.Map; @@ -28,9 +29,10 @@ namespace Content.Server.Chemistry.TileReactions if (puddle != null) { - var slippery = IoCManager.Resolve().GetComponent(puddle.Owner); + var entityManager = IoCManager.Resolve(); + var slippery = entityManager.GetComponent(puddle.Owner); slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier; - slippery.RequiredSlipSpeed = _requiredSlipSpeed; + EntitySystem.Get().SetRequiredTriggerSpeed(puddle.Owner, _requiredSlipSpeed); slippery.ParalyzeTime = _paralyzeTime; return reactVolume; diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index cb30eab6cb..a6375b2918 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -9,9 +9,11 @@ using Content.Server.Connection; using Content.Server.Database; using Content.Server.EUI; using Content.Server.GameTicking; +using Content.Server.GhostKick; using Content.Server.GuideGenerator; using Content.Server.Info; using Content.Server.IoC; +using Content.Server.LandMines; using Content.Server.Maps; using Content.Server.NodeContainer.NodeGroups; using Content.Server.Preferences.Managers; @@ -85,6 +87,8 @@ namespace Content.Server.Entry IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); + _voteManager.Initialize(); } } diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index 4ea7192008..deb35a976e 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -4,7 +4,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Examine; using Content.Shared.FixedPoint; using Content.Shared.Fluids; -using Content.Shared.Slippery; +using Content.Shared.StepTrigger; using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.Player; @@ -16,6 +16,7 @@ namespace Content.Server.Fluids.EntitySystems { [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly FluidSpreaderSystem _fluidSpreaderSystem = default!; + [Dependency] private readonly StepTriggerSystem _stepTrigger = default!; public override void Initialize() { @@ -71,20 +72,20 @@ namespace Content.Server.Fluids.EntitySystems { if ((puddleComponent.SlipThreshold == FixedPoint2.New(-1) || puddleComponent.CurrentVolume < puddleComponent.SlipThreshold) && - EntityManager.TryGetComponent(entityUid, out SlipperyComponent? oldSlippery)) + TryComp(entityUid, out StepTriggerComponent? stepTrigger)) { - oldSlippery.Slippery = false; + _stepTrigger.SetActive(entityUid, false, stepTrigger); } else if (puddleComponent.CurrentVolume >= puddleComponent.SlipThreshold) { - var newSlippery = EntityManager.EnsureComponent(entityUid); - newSlippery.Slippery = true; + var comp = EnsureComp(entityUid); + _stepTrigger.SetActive(entityUid, true, comp); } } private void HandlePuddleExamined(EntityUid uid, PuddleComponent component, ExaminedEvent args) { - if (EntityManager.TryGetComponent(uid, out var slippery) && slippery.Slippery) + if (TryComp(uid, out var slippery) && slippery.Active) { args.PushText(Loc.GetString("puddle-component-examine-is-slipper-text")); } diff --git a/Content.Server/GhostKick/GhostKickManager.cs b/Content.Server/GhostKick/GhostKickManager.cs new file mode 100644 index 0000000000..4d663caa5a --- /dev/null +++ b/Content.Server/GhostKick/GhostKickManager.cs @@ -0,0 +1,76 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.GhostKick; +using Robust.Server.Player; +using Robust.Shared.Console; +using Robust.Shared.Network; +using Robust.Shared.Timing; + +namespace Content.Server.GhostKick; + +// Handles logic for "ghost kicking". +// Basically we boot the client off the server without telling them, so the game shits itself. +// Hilariously isn't it? + +public sealed class GhostKickManager +{ + [Dependency] private readonly IServerNetManager _netManager = default!; + + public void Initialize() + { + _netManager.RegisterNetMessage(); + } + + public void DoDisconnect(INetChannel channel, string reason) + { + Timer.Spawn(TimeSpan.FromMilliseconds(100), () => + { + if (!channel.IsConnected) + return; + + // We do this so the client can set net.fakeloss 1 before getting ghosted. + // This avoids it spamming messages at the server that cause warnings due to unconnected client. + channel.SendMessage(new MsgGhostKick()); + + Timer.Spawn(TimeSpan.FromMilliseconds(100), () => + { + if (!channel.IsConnected) + return; + + // Actually just remove the client entirely. + channel.Disconnect(reason, false); + }); + }); + } +} + +[AdminCommand(AdminFlags.Admin)] +public sealed class GhostKickCommand : IConsoleCommand +{ + public string Command => "ghostkick"; + public string Description => "Kick a client from the server as if their network just dropped."; + public string Help => "Usage: ghostkick [Reason]"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 1) + { + shell.WriteError("Need at least one argument"); + return; + } + + var playerName = args[0]; + var reason = args.Length > 1 ? args[1] : "Ghost kicked by console"; + + var players = IoCManager.Resolve(); + var ghostKick = IoCManager.Resolve(); + + if (!players.TryGetSessionByUsername(playerName, out var player)) + { + shell.WriteError($"Unable to find player: '{playerName}'."); + return; + } + + ghostKick.DoDisconnect(player.ConnectedClient, reason); + } +} diff --git a/Content.Server/GhostKick/GhostKickUserOnTriggerComponent.cs b/Content.Server/GhostKick/GhostKickUserOnTriggerComponent.cs new file mode 100644 index 0000000000..dcbf72deb8 --- /dev/null +++ b/Content.Server/GhostKick/GhostKickUserOnTriggerComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.GhostKick; + +[RegisterComponent] +public sealed class GhostKickUserOnTriggerComponent : Component +{ + +} diff --git a/Content.Server/GhostKick/GhostKickUserOnTriggerSystem.cs b/Content.Server/GhostKick/GhostKickUserOnTriggerSystem.cs new file mode 100644 index 0000000000..8306400c34 --- /dev/null +++ b/Content.Server/GhostKick/GhostKickUserOnTriggerSystem.cs @@ -0,0 +1,24 @@ +using Content.Server.Explosion.EntitySystems; +using Robust.Server.GameObjects; + +namespace Content.Server.GhostKick; + +public sealed class GhostKickUserOnTriggerSystem : EntitySystem +{ + [Dependency] private readonly GhostKickManager _ghostKickManager = default!; + + public override void Initialize() + { + SubscribeLocalEvent(HandleMineTriggered); + } + + private void HandleMineTriggered(EntityUid uid, GhostKickUserOnTriggerComponent userOnTriggerComponent, TriggerEvent args) + { + if (!TryComp(args.User, out ActorComponent? actor)) + return; + + _ghostKickManager.DoDisconnect( + actor.PlayerSession.ConnectedClient, + "Tripped over a kick mine, crashed through the fourth wall"); + } +} diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 0c90be9f1a..0ed41c6b3d 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -9,6 +9,7 @@ using Content.Server.Chat.Managers; using Content.Server.Connection; using Content.Server.Database; using Content.Server.EUI; +using Content.Server.GhostKick; using Content.Server.Info; using Content.Server.Maps; using Content.Server.Module; @@ -52,6 +53,7 @@ namespace Content.Server.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Server/LandMines/LandMineComponent.cs b/Content.Server/LandMines/LandMineComponent.cs new file mode 100644 index 0000000000..2d2024ee63 --- /dev/null +++ b/Content.Server/LandMines/LandMineComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.LandMines; + +[RegisterComponent] +public sealed class LandMineComponent : Component +{ + +} diff --git a/Content.Server/LandMines/LandMineSystem.cs b/Content.Server/LandMines/LandMineSystem.cs new file mode 100644 index 0000000000..c531ba265e --- /dev/null +++ b/Content.Server/LandMines/LandMineSystem.cs @@ -0,0 +1,40 @@ +using Content.Server.Explosion.EntitySystems; +using Content.Shared.Popups; +using Content.Shared.StepTrigger; +using Robust.Shared.Player; + +namespace Content.Server.LandMines; + +public sealed class LandMineSystem : EntitySystem +{ + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly TriggerSystem _trigger = default!; + + + public override void Initialize() + { + SubscribeLocalEvent(HandleTriggered); + SubscribeLocalEvent(HandleTriggerAttempt); + } + + private static void HandleTriggerAttempt( + EntityUid uid, + LandMineComponent component, + ref StepTriggerAttemptEvent args) + { + args.Continue = true; + } + + private void HandleTriggered(EntityUid uid, LandMineComponent component, ref StepTriggeredEvent args) + { + _popupSystem.PopupCoordinates( + Loc.GetString("land-mine-triggered", ("mine", uid)), + Transform(uid).Coordinates, + Filter.Entities(args.Tripper)); + + _trigger.Trigger(uid, args.Tripper); + + QueueDel(uid); + } +} + diff --git a/Content.Shared/GhostKick/MsgGhostKick.cs b/Content.Shared/GhostKick/MsgGhostKick.cs new file mode 100644 index 0000000000..475c0692ff --- /dev/null +++ b/Content.Shared/GhostKick/MsgGhostKick.cs @@ -0,0 +1,17 @@ +using Lidgren.Network; +using Robust.Shared.Network; + +namespace Content.Shared.GhostKick; + +public sealed class MsgGhostKick : NetMessage +{ + public override MsgGroups MsgGroup => MsgGroups.Core; + + public override void ReadFromBuffer(NetIncomingMessage buffer) + { + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + } +} diff --git a/Content.Shared/Slippery/SharedSlipperySystem.cs b/Content.Shared/Slippery/SharedSlipperySystem.cs index 979eb8961c..bf7c2c09dc 100644 --- a/Content.Shared/Slippery/SharedSlipperySystem.cs +++ b/Content.Shared/Slippery/SharedSlipperySystem.cs @@ -1,13 +1,11 @@ -using System.Linq; using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.Inventory; using Content.Shared.StatusEffect; +using Content.Shared.StepTrigger; using Content.Shared.Stunnable; using JetBrains.Annotations; using Robust.Shared.Containers; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Dynamics; namespace Content.Shared.Slippery { @@ -17,144 +15,68 @@ namespace Content.Shared.Slippery [Dependency] private readonly SharedAdminLogSystem _adminLog = default!; [Dependency] private readonly SharedStunSystem _stunSystem = default!; [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!; - - private readonly List _slipped = new(); + [Dependency] private readonly SharedContainerSystem _container = default!; public override void Initialize() { base.Initialize(); - UpdatesOutsidePrediction = true; - - SubscribeLocalEvent(HandleCollide); + SubscribeLocalEvent(HandleAttemptCollide); + SubscribeLocalEvent(HandleStepTrigger); SubscribeLocalEvent(OnNoSlipAttempt); } - private void HandleCollide(EntityUid uid, SlipperyComponent component, StartCollideEvent args) + private void HandleStepTrigger(EntityUid uid, SlipperyComponent component, ref StepTriggeredEvent args) { - var otherUid = args.OtherFixture.Body.Owner; - - if (!CanSlip(component, otherUid)) return; - - if (!_slipped.Contains(component)) - _slipped.Add(component); - - component.Colliding.Add(otherUid); + TrySlip(component, args.Tripper); } - private void OnNoSlipAttempt(EntityUid uid, NoSlipComponent component, SlipAttemptEvent args) + private void HandleAttemptCollide( + EntityUid uid, + SlipperyComponent component, + ref StepTriggerAttemptEvent args) + { + args.Continue |= CanSlip(uid, args.Tripper); + } + + private static void OnNoSlipAttempt(EntityUid uid, NoSlipComponent component, SlipAttemptEvent args) { args.Cancel(); } - /// - public override void Update(float frameTime) + private bool CanSlip(EntityUid uid, EntityUid toSlip) { - for (var i = _slipped.Count - 1; i >= 0; i--) - { - var slipperyComp = _slipped[i]; - if (!Update(slipperyComp)) continue; - _slipped.RemoveAt(i); - } + return !_container.IsEntityInContainer(uid) + && _statusEffectsSystem.CanApplyEffect(toSlip, "Stun"); //Should be KnockedDown instead? } - public bool CanSlip(SlipperyComponent component, EntityUid uid) + private void TrySlip(SlipperyComponent component, EntityUid other) { - if (!component.Slippery - || component.Owner.IsInContainer() - || component.Slipped.Contains(uid) - || !_statusEffectsSystem.CanApplyEffect(uid, "Stun")) //Should be KnockedDown instead? - { - return false; - } - - return true; - } - - private bool TrySlip(SlipperyComponent component, IPhysBody ourBody, IPhysBody otherBody) - { - if (!CanSlip(component, otherBody.Owner)) return false; - - if (otherBody.LinearVelocity.Length < component.RequiredSlipSpeed) - { - return false; - } - - var percentage = otherBody.GetWorldAABB().IntersectPercentage(ourBody.GetWorldAABB()); - - if (percentage < component.IntersectPercentage) - { - return false; - } - - if (EntityManager.HasComponent(otherBody.Owner)) - { - return false; - } + if (HasComp(other)) + return; var ev = new SlipAttemptEvent(); - RaiseLocalEvent(otherBody.Owner, ev, false); + RaiseLocalEvent(other, ev, false); if (ev.Cancelled) - return false; + return; - otherBody.LinearVelocity *= component.LaunchForwardsMultiplier; + if (TryComp(other, out PhysicsComponent? physics)) + physics.LinearVelocity *= component.LaunchForwardsMultiplier; - bool playSound = !_statusEffectsSystem.HasStatusEffect(otherBody.Owner, "KnockedDown"); + var playSound = !_statusEffectsSystem.HasStatusEffect(other, "KnockedDown"); - _stunSystem.TryParalyze(otherBody.Owner, TimeSpan.FromSeconds(component.ParalyzeTime), true); - component.Slipped.Add(otherBody.Owner); - component.Dirty(); + _stunSystem.TryParalyze(other, TimeSpan.FromSeconds(component.ParalyzeTime), true); - //Preventing from playing the slip sound when you are already knocked down. - if(playSound) - { + // Preventing from playing the slip sound when you are already knocked down. + if (playSound) PlaySound(component); - } - _adminLog.Add(LogType.Slip, LogImpact.Low, $"{ToPrettyString(otherBody.Owner):mob} slipped on collision with {ToPrettyString(component.Owner):entity}"); - - return true; + _adminLog.Add(LogType.Slip, LogImpact.Low, + $"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(component.Owner):entity}"); } // Until we get predicted slip sounds TM? protected abstract void PlaySound(SlipperyComponent component); - - private bool Update(SlipperyComponent component) - { - if (component.Deleted || !component.Slippery || component.Colliding.Count == 0) - return true; - - if (!EntityManager.TryGetComponent(component.Owner, out PhysicsComponent? body)) - { - component.Colliding.Clear(); - return true; - } - - foreach (var uid in component.Colliding.ToArray()) - { - if (!uid.IsValid()) - { - component.Colliding.Remove(uid); - component.Slipped.Remove(uid); - component.Dirty(); - continue; - } - - if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? otherPhysics) || - !body.GetWorldAABB().Intersects(otherPhysics.GetWorldAABB())) - { - component.Colliding.Remove(uid); - component.Slipped.Remove(uid); - component.Dirty(); - continue; - } - - if (!component.Slipped.Contains(uid)) - TrySlip(component, body, otherPhysics); - } - - return false; - } } /// diff --git a/Content.Shared/Slippery/SlipperyComponent.cs b/Content.Shared/Slippery/SlipperyComponent.cs index e366342054..7cd8c1e028 100644 --- a/Content.Shared/Slippery/SlipperyComponent.cs +++ b/Content.Shared/Slippery/SlipperyComponent.cs @@ -1,31 +1,26 @@ using System.Linq; using Content.Shared.Sound; +using Content.Shared.Sound; +using Content.Shared.StepTrigger; using Robust.Shared.GameStates; using Robust.Shared.Serialization; namespace Content.Shared.Slippery { + /// + /// Causes somebody to slip when they walk over this entity. + /// + /// + /// Requires , see that component for some additional properties. + /// [RegisterComponent] - [NetworkedComponent()] + [NetworkedComponent] public sealed class SlipperyComponent : Component { private float _paralyzeTime = 3f; - private float _intersectPercentage = 0.3f; - private float _requiredSlipSpeed = 3.5f; private float _launchForwardsMultiplier = 1f; - private bool _slippery = true; private SoundSpecifier _slipSound = new SoundPathSpecifier("/Audio/Effects/slip.ogg"); - /// - /// List of entities that are currently colliding with the entity. - /// - public readonly HashSet Colliding = new(); - - /// - /// The list of entities that have been slipped by this component, which shouldn't be slipped again. - /// - public readonly HashSet Slipped = new(); - /// /// Path to the sound to be played when a mob slips. /// @@ -61,40 +56,6 @@ namespace Content.Shared.Slippery } } - /// - /// Percentage of shape intersection for a slip to occur. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("intersectPercentage")] - public float IntersectPercentage - { - get => _intersectPercentage; - set - { - if (MathHelper.CloseToPercent(_intersectPercentage, value)) return; - - _intersectPercentage = value; - Dirty(); - } - } - - /// - /// Entities will only be slipped if their speed exceeds this limit. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("requiredSlipSpeed")] - public float RequiredSlipSpeed - { - get => _requiredSlipSpeed; - set - { - if (MathHelper.CloseToPercent(_requiredSlipSpeed, value)) return; - - _requiredSlipSpeed = value; - Dirty(); - } - } - /// /// The entity's speed will be multiplied by this to slip it forwards. /// @@ -112,44 +73,18 @@ namespace Content.Shared.Slippery } } - /// - /// Whether or not this component will try to slip entities. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("slippery")] - public bool Slippery - { - get => _slippery; - set - { - if (_slippery == value) return; - - _slippery = value; - Dirty(); - } - } - public override ComponentState GetComponentState() { - return new SlipperyComponentState(ParalyzeTime, IntersectPercentage, RequiredSlipSpeed, LaunchForwardsMultiplier, Slippery, SlipSound.GetSound(), Slipped.ToArray()); + return new SlipperyComponentState(ParalyzeTime, LaunchForwardsMultiplier, SlipSound.GetSound()); } public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) { if (curState is not SlipperyComponentState state) return; - _slippery = state.Slippery; - _intersectPercentage = state.IntersectPercentage; _paralyzeTime = state.ParalyzeTime; - _requiredSlipSpeed = state.RequiredSlipSpeed; _launchForwardsMultiplier = state.LaunchForwardsMultiplier; _slipSound = new SoundPathSpecifier(state.SlipSound); - Slipped.Clear(); - - foreach (var slipped in state.Slipped) - { - Slipped.Add(slipped); - } } } @@ -157,22 +92,14 @@ namespace Content.Shared.Slippery public sealed class SlipperyComponentState : ComponentState { public float ParalyzeTime { get; } - public float IntersectPercentage { get; } - public float RequiredSlipSpeed { get; } public float LaunchForwardsMultiplier { get; } - public bool Slippery { get; } public string SlipSound { get; } - public readonly EntityUid[] Slipped; - public SlipperyComponentState(float paralyzeTime, float intersectPercentage, float requiredSlipSpeed, float launchForwardsMultiplier, bool slippery, string slipSound, EntityUid[] slipped) + public SlipperyComponentState(float paralyzeTime, float launchForwardsMultiplier, string slipSound) { ParalyzeTime = paralyzeTime; - IntersectPercentage = intersectPercentage; - RequiredSlipSpeed = requiredSlipSpeed; LaunchForwardsMultiplier = launchForwardsMultiplier; - Slippery = slippery; SlipSound = slipSound; - Slipped = slipped; } } } diff --git a/Content.Shared/StepTrigger/StepTriggerComponent.cs b/Content.Shared/StepTrigger/StepTriggerComponent.cs new file mode 100644 index 0000000000..c54e77e786 --- /dev/null +++ b/Content.Shared/StepTrigger/StepTriggerComponent.cs @@ -0,0 +1,62 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.StepTrigger; + +[RegisterComponent] +[NetworkedComponent] +[Friend(typeof(StepTriggerSystem))] +public sealed class StepTriggerComponent : Component +{ + /// + /// List of entities that are currently colliding with the entity. + /// + public readonly HashSet Colliding = new(); + + /// + /// The list of entities that are standing on this entity, + /// which shouldn't be able to trigger it again until stepping off. + /// + public readonly HashSet CurrentlySteppedOn = new(); + + /// + /// Whether or not this component will currently try to trigger for entities. + /// + [DataField("active")] + public bool Active { get; set; } = true; + + /// + /// Ratio of shape intersection for a trigger to occur. + /// + [DataField("intersectRatio")] + public float IntersectRatio { get; set; } = 0.3f; + + /// + /// Entities will only be triggered if their speed exceeds this limit. + /// + [DataField("requiredTriggeredSpeed")] + public float RequiredTriggerSpeed { get; set; } = 3.5f; +} + +[RegisterComponent] +[Friend(typeof(StepTriggerSystem))] +public sealed class StepTriggerActiveComponent : Component +{ + +} + +[Serializable, NetSerializable] +public sealed class StepTriggerComponentState : ComponentState +{ + public float IntersectRatio { get; } + public float RequiredTriggerSpeed { get; } + public readonly EntityUid[] CurrentlySteppedOn; + + public StepTriggerComponentState(float intersectRatio, EntityUid[] currentlySteppedOn, float requiredTriggerSpeed) + { + IntersectRatio = intersectRatio; + CurrentlySteppedOn = currentlySteppedOn; + RequiredTriggerSpeed = requiredTriggerSpeed; + } +} + diff --git a/Content.Shared/StepTrigger/StepTriggerSystem.cs b/Content.Shared/StepTrigger/StepTriggerSystem.cs new file mode 100644 index 0000000000..5031bdd7a1 --- /dev/null +++ b/Content.Shared/StepTrigger/StepTriggerSystem.cs @@ -0,0 +1,177 @@ +using System.Linq; +using Robust.Shared.GameStates; +using Robust.Shared.Physics.Dynamics; + +namespace Content.Shared.StepTrigger; + +public sealed class StepTriggerSystem : EntitySystem +{ + [Dependency] private readonly EntityLookupSystem _entityLookup = default!; + + public override void Initialize() + { + SubscribeLocalEvent(TriggerGetState); + SubscribeLocalEvent(TriggerHandleState); + + SubscribeLocalEvent(HandleCollide); + } + + public override void Update(float frameTime) + { + foreach (var (_, trigger) in EntityQuery()) + { + if (!Update(trigger)) + continue; + + RemComp(trigger.Owner); + } + } + + private bool Update(StepTriggerComponent component) + { + if (component.Deleted || !component.Active || component.Colliding.Count == 0) + return true; + + foreach (var otherUid in component.Colliding.ToArray()) + { + if (!otherUid.IsValid()) + { + component.Colliding.Remove(otherUid); + component.CurrentlySteppedOn.Remove(otherUid); + component.Dirty(); + continue; + } + + // TODO: This shouldn't be calculating based on world AABBs. + var ourAabb = _entityLookup.GetWorldAABB(component.Owner); + var otherAabb = _entityLookup.GetWorldAABB(otherUid); + + if (!TryComp(otherUid, out PhysicsComponent? otherPhysics) || !ourAabb.Intersects(otherAabb)) + { + component.Colliding.Remove(otherUid); + component.CurrentlySteppedOn.Remove(otherUid); + component.Dirty(); + continue; + } + + if (component.CurrentlySteppedOn.Contains(otherUid)) + continue; + + if (!CanTrigger(component.Owner, otherUid, component)) + continue; + + if (otherPhysics.LinearVelocity.Length < component.RequiredTriggerSpeed) + continue; + + var percentage = otherAabb.IntersectPercentage(ourAabb); + if (percentage < component.IntersectRatio) + continue; + + var ev = new StepTriggeredEvent { Source = component.Owner, Tripper = otherUid }; + RaiseLocalEvent(component.Owner, ref ev); + + component.CurrentlySteppedOn.Add(otherUid); + component.Dirty(); + } + + return false; + } + + private bool CanTrigger(EntityUid uid, EntityUid otherUid, StepTriggerComponent component) + { + if (!component.Active || component.CurrentlySteppedOn.Contains(otherUid)) + return false; + + var msg = new StepTriggerAttemptEvent { Source = uid, Tripper = otherUid }; + + RaiseLocalEvent(uid, ref msg); + + return msg.Continue; + } + + private void HandleCollide(EntityUid uid, StepTriggerComponent component, StartCollideEvent args) + { + var otherUid = args.OtherFixture.Body.Owner; + + if (!CanTrigger(uid, otherUid, component)) + return; + + EnsureComp(uid); + + component.Colliding.Add(otherUid); + } + + private static void TriggerHandleState(EntityUid uid, StepTriggerComponent component, ref ComponentHandleState args) + { + if (args.Current is not StepTriggerComponentState state) + return; + + component.RequiredTriggerSpeed = state.RequiredTriggerSpeed; + component.IntersectRatio = state.IntersectRatio; + component.CurrentlySteppedOn.Clear(); + + foreach (var slipped in state.CurrentlySteppedOn) + { + component.CurrentlySteppedOn.Add(slipped); + } + } + + private static void TriggerGetState(EntityUid uid, StepTriggerComponent component, ref ComponentGetState args) + { + args.State = new StepTriggerComponentState( + component.IntersectRatio, + component.CurrentlySteppedOn.ToArray(), + component.RequiredTriggerSpeed); + } + + public void SetIntersectRatio(EntityUid uid, float ratio, StepTriggerComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (MathHelper.CloseToPercent(component.IntersectRatio, ratio)) + return; + + component.IntersectRatio = ratio; + Dirty(component); + } + + public void SetRequiredTriggerSpeed(EntityUid uid, float speed, StepTriggerComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (MathHelper.CloseToPercent(component.RequiredTriggerSpeed, speed)) + return; + + component.RequiredTriggerSpeed = speed; + Dirty(component); + } + + public void SetActive(EntityUid uid, bool active, StepTriggerComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (active == component.Active) + return; + + component.Active = active; + Dirty(component); + } +} + +[ByRefEvent] +public struct StepTriggerAttemptEvent +{ + public EntityUid Source; + public EntityUid Tripper; + public bool Continue; +} + +[ByRefEvent] +public struct StepTriggeredEvent +{ + public EntityUid Source; + public EntityUid Tripper; +} diff --git a/Resources/Locale/en-US/land-mines/land-mines.ftl b/Resources/Locale/en-US/land-mines/land-mines.ftl new file mode 100644 index 0000000000..b00c9fd2b5 --- /dev/null +++ b/Resources/Locale/en-US/land-mines/land-mines.ftl @@ -0,0 +1 @@ +land-mine-triggered = You step on the { $mine }! diff --git a/Resources/Prototypes/Entities/Effects/chemistry_effects.yml b/Resources/Prototypes/Entities/Effects/chemistry_effects.yml index 23196f68f9..43b7eb1f7e 100644 --- a/Resources/Prototypes/Entities/Effects/chemistry_effects.yml +++ b/Resources/Prototypes/Entities/Effects/chemistry_effects.yml @@ -60,6 +60,7 @@ maxVol: 600 canReact: false - type: Slippery + - type: StepTrigger - type: entity id: IronMetalFoam diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index 4ed14b030d..9860a7a47c 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -12,7 +12,7 @@ spillSound: path: /Audio/Effects/Fluids/splat.ogg recolor: true - - type: Clickable + - type: Clickable - type: Evaporation - type: Physics - type: Fixtures @@ -57,6 +57,7 @@ - type: PuddleVisualizer - type: Slippery launchForwardsMultiplier: 2.0 + - type: StepTrigger - type: entity name: puddle @@ -75,7 +76,8 @@ recolor: true - type: Slippery launchForwardsMultiplier: 2.0 - + - type: StepTrigger + - type: entity name: puddle id: PuddleSplatter @@ -92,7 +94,8 @@ - type: PuddleVisualizer - type: Slippery launchForwardsMultiplier: 2.0 - + - type: StepTrigger + - type: entity id: PuddleBlood name: blood @@ -137,7 +140,8 @@ - type: PuddleVisualizer - type: Slippery launchForwardsMultiplier: 2.0 - + - type: StepTrigger + - type: entity name: toxins vomit id: PuddleVomitToxin @@ -162,7 +166,8 @@ - type: PuddleVisualizer - type: Slippery launchForwardsMultiplier: 2.0 - + - type: StepTrigger + - type: entity name: writing id: PuddleWriting @@ -180,4 +185,4 @@ - type: PuddleVisualizer - type: Slippery launchForwardsMultiplier: 2.0 - + - type: StepTrigger diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index 2c61b625fe..d7a5accc96 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -177,8 +177,9 @@ sprite: Objects/Specific/Hydroponics/banana.rsi HeldPrefix: peel - type: Slippery - intersectPercentage: 0.2 launchForwardsMultiplier: 6.0 + - type: StepTrigger + intersectRatio: 0.2 - type: CollisionWake enabled: false - type: Physics diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index b0a70146cd..e57306910b 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -193,6 +193,7 @@ - type: Slippery paralyzeTime: 4 launchForwardsMultiplier: 9.0 + - type: StepTrigger - type: CollisionWake enabled: false - type: Physics @@ -362,7 +363,7 @@ - type: PDA id: CaptainIDCard penSlot: - startingItem: PenCap + startingItem: PenCap - type: Appearance visuals: - type: PDAVisualizer @@ -379,7 +380,7 @@ - type: PDA id: HoPIDCard penSlot: - startingItem: PenHop + startingItem: PenHop - type: Appearance visuals: - type: PDAVisualizer diff --git a/Resources/Prototypes/Entities/Objects/Misc/land_mine.yml b/Resources/Prototypes/Entities/Objects/Misc/land_mine.yml new file mode 100644 index 0000000000..7fe2ca851a --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Misc/land_mine.yml @@ -0,0 +1,46 @@ +- type: entity + id: BaseLandMine + abstract: true + components: + - type: Clickable + - type: InteractionOutline + - type: MovedByPressure + - type: Physics + bodyType: Static + fixedRotation: true + - type: Fixtures + fixtures: + - shape: + !type:PhysShapeAabb + bounds: "-0.2,-0.2,0.2,0.2" + id: "slips" + hard: false + layer: + - LowImpassable + - type: Sprite + drawdepth: FloorObjects + sprite: Objects/Misc/uglymine.rsi + state: uglymine + - type: LandMine + - type: StepTrigger + requiredTriggeredSpeed: 0 + +- type: entity + name: kick mine + parent: BaseLandMine + id: LandMineKick + components: + - type: GhostKickUserOnTrigger + +- type: entity + name: explosive mine + parent: BaseLandMine + id: LandMineExplosive + components: + - type: ExplodeOnTrigger + - type: Explosive + explosionType: Default + maxIntensity: 10 + intensitySlope: 3 + totalIntensity: 120 # about a ~4 tile radius + canCreateVacuum: false diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml index 52ca8dfc6f..d30c061533 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml @@ -14,8 +14,9 @@ sprite: Objects/Specific/Janitorial/soap.rsi - type: Slippery paralyzeTime: 2 - intersectPercentage: 0.2 launchForwardsMultiplier: 6.0 + - type: StepTrigger + intersectRatio: 0.2 - type: CollisionWake enabled: false - type: Physics @@ -69,6 +70,7 @@ - type: Slippery paralyzeTime: 5 launchForwardsMultiplier: 9.0 + - type: StepTrigger - type: Item HeldPrefix: syndie @@ -82,6 +84,7 @@ state: gibs - type: Slippery paralyzeTime: 2 + - type: StepTrigger - type: Item HeldPrefix: gibs @@ -96,5 +99,6 @@ - type: Slippery paralyzeTime: 7 launchForwardsMultiplier: 9.0 + - type: StepTrigger - type: Item HeldPrefix: omega diff --git a/Resources/Textures/Objects/Misc/uglymine.rsi/meta.json b/Resources/Textures/Objects/Misc/uglymine.rsi/meta.json new file mode 100644 index 0000000000..1a7dcae417 --- /dev/null +++ b/Resources/Textures/Objects/Misc/uglymine.rsi/meta.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "size": + { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/tgstation/tgstation/blob/4323540b6bec3ae93bbf13d685c2dbe0cb40a36e/icons/obj/items_and_weapons.dmi", + "states": + [ + { + "name": "uglymine", + "directions": 1, + "delays": + [ + [ + 0.3, + 0.1 + ] + ] + }, + { + "name": "uglymine-inactive", + "directions": 1, + "delays": + [ + [ + 0.3, + 0.1 + ] + ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Misc/uglymine.rsi/uglymine-inactive.png b/Resources/Textures/Objects/Misc/uglymine.rsi/uglymine-inactive.png new file mode 100644 index 0000000000..99157cd53b Binary files /dev/null and b/Resources/Textures/Objects/Misc/uglymine.rsi/uglymine-inactive.png differ diff --git a/Resources/Textures/Objects/Misc/uglymine.rsi/uglymine.png b/Resources/Textures/Objects/Misc/uglymine.rsi/uglymine.png new file mode 100644 index 0000000000..52da78b13b Binary files /dev/null and b/Resources/Textures/Objects/Misc/uglymine.rsi/uglymine.png differ