Kick mines (real) (#8056)

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
Pieter-Jan Briers
2022-05-18 06:07:35 +02:00
committed by GitHub
parent 2f604ce05c
commit ebfe5e888f
29 changed files with 635 additions and 220 deletions

View File

@@ -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<ChangelogManager>().Initialize();
IoCManager.Resolve<RulesManager>().Initialize();
IoCManager.Resolve<ViewportManager>().Initialize();
IoCManager.Resolve<GhostKickManager>().Initialize();
IoCManager.InjectDependencies(this);

View File

@@ -321,6 +321,8 @@ namespace Content.Client.Entry
"Armor",
"AtmosMonitor",
"AtmosAlarmable",
"LandMine",
"GhostKickUserOnTrigger",
"FireAlarm",
"AirAlarm",
"RadarConsole",

View File

@@ -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<MsgGhostKick>(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);
}
}

View File

@@ -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<ViewportManager, ViewportManager>();
IoCManager.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
IoCManager.Register<NetworkResourceManager>();
IoCManager.Register<GhostKickManager>();
}
}
}

View File

@@ -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
/// <remarks>
/// Yes that's right it's not technically MapCoordinates.
/// </remarks>
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)

View File

@@ -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<IEntityManager>().GetComponent<SlipperyComponent>(puddle.Owner);
var entityManager = IoCManager.Resolve<IEntityManager>();
var slippery = entityManager.GetComponent<SlipperyComponent>(puddle.Owner);
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;
slippery.RequiredSlipSpeed = _requiredSlipSpeed;
EntitySystem.Get<StepTriggerSystem>().SetRequiredTriggerSpeed(puddle.Owner, _requiredSlipSpeed);
slippery.ParalyzeTime = _paralyzeTime;
return reactVolume;

View File

@@ -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<INodeGroupFactory>().Initialize();
IoCManager.Resolve<IGamePrototypeLoadManager>().Initialize();
IoCManager.Resolve<NetworkResourceManager>().Initialize();
IoCManager.Resolve<GhostKickManager>().Initialize();
_voteManager.Initialize();
}
}

View File

@@ -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<SlipperyComponent>(entityUid);
newSlippery.Slippery = true;
var comp = EnsureComp<StepTriggerComponent>(entityUid);
_stepTrigger.SetActive(entityUid, true, comp);
}
}
private void HandlePuddleExamined(EntityUid uid, PuddleComponent component, ExaminedEvent args)
{
if (EntityManager.TryGetComponent<SlipperyComponent>(uid, out var slippery) && slippery.Slippery)
if (TryComp<StepTriggerComponent>(uid, out var slippery) && slippery.Active)
{
args.PushText(Loc.GetString("puddle-component-examine-is-slipper-text"));
}

View File

@@ -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<MsgGhostKick>();
}
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 <Player> [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<IPlayerManager>();
var ghostKick = IoCManager.Resolve<GhostKickManager>();
if (!players.TryGetSessionByUsername(playerName, out var player))
{
shell.WriteError($"Unable to find player: '{playerName}'.");
return;
}
ghostKick.DoDisconnect(player.ConnectedClient, reason);
}
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.GhostKick;
[RegisterComponent]
public sealed class GhostKickUserOnTriggerComponent : Component
{
}

View File

@@ -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<GhostKickUserOnTriggerComponent, TriggerEvent>(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");
}
}

View File

@@ -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<RoleBanManager, RoleBanManager>();
IoCManager.Register<NetworkResourceManager>();
IoCManager.Register<IAdminNotesManager, AdminNotesManager>();
IoCManager.Register<GhostKickManager>();
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.LandMines;
[RegisterComponent]
public sealed class LandMineComponent : Component
{
}

View File

@@ -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<LandMineComponent, StepTriggeredEvent>(HandleTriggered);
SubscribeLocalEvent<LandMineComponent, StepTriggerAttemptEvent>(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);
}
}

View File

@@ -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)
{
}
}

View File

@@ -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<SlipperyComponent> _slipped = new();
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<SlipperyComponent, StartCollideEvent>(HandleCollide);
SubscribeLocalEvent<SlipperyComponent, StepTriggerAttemptEvent>(HandleAttemptCollide);
SubscribeLocalEvent<SlipperyComponent, StepTriggeredEvent>(HandleStepTrigger);
SubscribeLocalEvent<NoSlipComponent, SlipAttemptEvent>(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();
}
/// <inheritdoc />
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<KnockedDownComponent>(otherBody.Owner))
{
return false;
}
if (HasComp<KnockedDownComponent>(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;
}
}
/// <summary>

View File

@@ -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
{
/// <summary>
/// Causes somebody to slip when they walk over this entity.
/// </summary>
/// <remarks>
/// Requires <see cref="StepTriggerComponent"/>, see that component for some additional properties.
/// </remarks>
[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");
/// <summary>
/// List of entities that are currently colliding with the entity.
/// </summary>
public readonly HashSet<EntityUid> Colliding = new();
/// <summary>
/// The list of entities that have been slipped by this component, which shouldn't be slipped again.
/// </summary>
public readonly HashSet<EntityUid> Slipped = new();
/// <summary>
/// Path to the sound to be played when a mob slips.
/// </summary>
@@ -61,40 +56,6 @@ namespace Content.Shared.Slippery
}
}
/// <summary>
/// Percentage of shape intersection for a slip to occur.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("intersectPercentage")]
public float IntersectPercentage
{
get => _intersectPercentage;
set
{
if (MathHelper.CloseToPercent(_intersectPercentage, value)) return;
_intersectPercentage = value;
Dirty();
}
}
/// <summary>
/// Entities will only be slipped if their speed exceeds this limit.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("requiredSlipSpeed")]
public float RequiredSlipSpeed
{
get => _requiredSlipSpeed;
set
{
if (MathHelper.CloseToPercent(_requiredSlipSpeed, value)) return;
_requiredSlipSpeed = value;
Dirty();
}
}
/// <summary>
/// The entity's speed will be multiplied by this to slip it forwards.
/// </summary>
@@ -112,44 +73,18 @@ namespace Content.Shared.Slippery
}
}
/// <summary>
/// Whether or not this component will try to slip entities.
/// </summary>
[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;
}
}
}

View File

@@ -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
{
/// <summary>
/// List of entities that are currently colliding with the entity.
/// </summary>
public readonly HashSet<EntityUid> Colliding = new();
/// <summary>
/// The list of entities that are standing on this entity,
/// which shouldn't be able to trigger it again until stepping off.
/// </summary>
public readonly HashSet<EntityUid> CurrentlySteppedOn = new();
/// <summary>
/// Whether or not this component will currently try to trigger for entities.
/// </summary>
[DataField("active")]
public bool Active { get; set; } = true;
/// <summary>
/// Ratio of shape intersection for a trigger to occur.
/// </summary>
[DataField("intersectRatio")]
public float IntersectRatio { get; set; } = 0.3f;
/// <summary>
/// Entities will only be triggered if their speed exceeds this limit.
/// </summary>
[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;
}
}

View File

@@ -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<StepTriggerComponent, ComponentGetState>(TriggerGetState);
SubscribeLocalEvent<StepTriggerComponent, ComponentHandleState>(TriggerHandleState);
SubscribeLocalEvent<StepTriggerComponent, StartCollideEvent>(HandleCollide);
}
public override void Update(float frameTime)
{
foreach (var (_, trigger) in EntityQuery<StepTriggerActiveComponent, StepTriggerComponent>())
{
if (!Update(trigger))
continue;
RemComp<StepTriggerActiveComponent>(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<StepTriggerActiveComponent>(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;
}

View File

@@ -0,0 +1 @@
land-mine-triggered = You step on the { $mine }!

View File

@@ -60,6 +60,7 @@
maxVol: 600
canReact: false
- type: Slippery
- type: StepTrigger
- type: entity
id: IronMetalFoam

View File

@@ -57,6 +57,7 @@
- type: PuddleVisualizer
- type: Slippery
launchForwardsMultiplier: 2.0
- type: StepTrigger
- type: entity
name: puddle
@@ -75,6 +76,7 @@
recolor: true
- type: Slippery
launchForwardsMultiplier: 2.0
- type: StepTrigger
- type: entity
name: puddle
@@ -92,6 +94,7 @@
- type: PuddleVisualizer
- type: Slippery
launchForwardsMultiplier: 2.0
- type: StepTrigger
- type: entity
id: PuddleBlood
@@ -137,6 +140,7 @@
- type: PuddleVisualizer
- type: Slippery
launchForwardsMultiplier: 2.0
- type: StepTrigger
- type: entity
name: toxins vomit
@@ -162,6 +166,7 @@
- type: PuddleVisualizer
- type: Slippery
launchForwardsMultiplier: 2.0
- type: StepTrigger
- type: entity
name: writing
@@ -180,4 +185,4 @@
- type: PuddleVisualizer
- type: Slippery
launchForwardsMultiplier: 2.0
- type: StepTrigger

View File

@@ -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

View File

@@ -193,6 +193,7 @@
- type: Slippery
paralyzeTime: 4
launchForwardsMultiplier: 9.0
- type: StepTrigger
- type: CollisionWake
enabled: false
- type: Physics

View File

@@ -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

View File

@@ -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

View File

@@ -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
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B