human.AddFunction(EngineKeyFunctions.MoveLeft);
human.AddFunction(EngineKeyFunctions.MoveRight);
human.AddFunction(EngineKeyFunctions.Walk);
+ human.AddFunction(ContentKeyFunctions.ToggleKnockdown);
human.AddFunction(ContentKeyFunctions.SwapHands);
human.AddFunction(ContentKeyFunctions.SwapHandsReverse);
human.AddFunction(ContentKeyFunctions.Drop);
AddButton(EngineKeyFunctions.Walk);
AddCheckBox("ui-options-hotkey-toggle-walk", _cfg.GetCVar(CCVars.ToggleWalk), HandleToggleWalk);
InitToggleWalk();
+ AddButton(ContentKeyFunctions.ToggleKnockdown);
AddHeader("ui-options-header-camera");
AddButton(EngineKeyFunctions.CameraRotateLeft);
-using System.Numerics;
-using Content.Shared.Mobs;
+using System.Numerics;
+using Content.Shared.CombatMode;
+using Content.Shared.Interaction;
using Content.Shared.Stunnable;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
+using Robust.Shared.Input;
+using Robust.Shared.Input.Binding;
using Robust.Shared.Random;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
namespace Content.Client.Stunnable;
public sealed class StunSystem : SharedStunSystem
{
+ [Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<StunVisualsComponent, AppearanceChangeEvent>(OnAppearanceChanged);
+
+ CommandBinds.Builder
+ .BindAfter(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnUseSecondary, true, true), typeof(SharedInteractionSystem))
+ .Register<StunSystem>();
+ }
+
+ private bool OnUseSecondary(in PointerInputCmdHandler.PointerInputCmdArgs args)
+ {
+ if (args.Session?.AttachedEntity is not {Valid: true} uid)
+ return false;
+
+ if (args.EntityUid != uid || !HasComp<KnockedDownComponent>(uid) || !_combat.IsInCombatMode(uid))
+ return false;
+
+ RaisePredictiveEvent(new ForceStandUpEvent());
+ return true;
}
/// <summary>
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Slippery;
+using Content.Shared.Stunnable;
using Content.Shared.Tabletop.Components;
using Content.Shared.Tools.Systems;
using Content.Shared.Verbs;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
+using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
if (!hadSlipComponent)
{
slipComponent.SlipData.SuperSlippery = true;
- slipComponent.SlipData.ParalyzeTime = TimeSpan.FromSeconds(5);
+ slipComponent.SlipData.StunTime = TimeSpan.FromSeconds(5);
slipComponent.SlipData.LaunchForwardsMultiplier = 20;
}
Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
};
args.Verbs.Add(omniaccent);
+
+ var crawlerName = Loc.GetString("admin-smite-crawler-name").ToLowerInvariant();
+ Verb crawler = new()
+ {
+ Text = crawlerName,
+ Category = VerbCategory.Smite,
+ Icon = new SpriteSpecifier.Rsi(new("Mobs/Animals/snake.rsi"), "icon"),
+ Act = () =>
+ {
+ EnsureComp<WormComponent>(args.Target);
+ },
+ Impact = LogImpact.Extreme,
+ Message = string.Join(": ", crawlerName, Loc.GetString("admin-smite-crawler-description"))
+ };
+ args.Verbs.Add(crawler);
}
}
// Ensure we actually have the component
EnsureComp<TileFrictionModifierComponent>(entity);
+ EnsureComp<SlipperyComponent>(entity, out var slipComp);
+
// This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on
// the sprite threshold for a puddle larger than 5 pixels.
var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
var launchMult = FixedPoint2.Zero;
// A cumulative weighted amount of stun times from slippery reagents
var stunTimer = TimeSpan.Zero;
+ // A cumulative weighted amount of knockdown times from slippery reagents
+ var knockdownTimer = TimeSpan.Zero;
// Check if the puddle is big enough to slip in to avoid doing unnecessary logic
if (solution.Volume <= smallPuddleThreshold)
{
_stepTrigger.SetActive(entity, false, comp);
_tile.SetModifier(entity, 1f);
+ slipComp.SlipData.SlipFriction = 1f;
+ slipComp.AffectsSliding = false;
+ Dirty(entity, slipComp);
return;
}
- if (!TryComp<SlipperyComponent>(entity, out var slipComp))
- return;
+ slipComp.AffectsSliding = true;
foreach (var (reagent, quantity) in solution.Contents)
{
// Aggregate launch speed based on quantity
launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity;
// Aggregate stun times based on quantity
- stunTimer += reagentProto.SlipData.ParalyzeTime * (float)quantity;
+ stunTimer += reagentProto.SlipData.StunTime * (float)quantity;
+ knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity;
if (reagentProto.SlipData.SuperSlippery)
superSlipperyUnits += quantity;
// A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount.
if (slipperyUnits > 0)
{
- slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits);
- slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits;
+ slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
+ slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits);
+ slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits);
}
// Only make it super slippery if there is enough super slippery units for its own puddle
{
// TODO: Can probably predict this.
- // See stunsystem for what these do
- [DataField("stunAmount")]
- public int StunAmount;
+ /// <summary>
+ /// How long we are stunned for
+ /// </summary>
+ [DataField]
+ public TimeSpan StunAmount;
+
+ /// <summary>
+ /// How long we are knocked down for
+ /// </summary>
+ [DataField]
+ public TimeSpan KnockdownAmount;
+
+ /// <summary>
+ /// How long we are slowed down for
+ /// </summary>
+ [DataField]
+ public TimeSpan SlowdownAmount;
- [DataField("knockdownAmount")]
- public int KnockdownAmount;
+ /// <summary>
+ /// Multiplier for a mob's walking speed
+ /// </summary>
+ [DataField]
+ public float WalkSpeedModifier = 1f;
- [DataField("slowdownAmount")]
- public int SlowdownAmount;
+ /// <summary>
+ /// Multiplier for a mob's sprinting speed
+ /// </summary>
+ [DataField]
+ public float SprintSpeedModifier = 1f;
- [DataField("walkSpeedMultiplier")]
- public float WalkSpeedMultiplier = 1f;
+ /// <summary>
+ /// Refresh Stun or Slowdown on hit
+ /// </summary>
+ [DataField]
+ public bool Refresh = true;
- [DataField("runSpeedMultiplier")]
- public float RunSpeedMultiplier = 1f;
+ /// <summary>
+ /// Should the entity try and stand automatically after being knocked down?
+ /// </summary>
+ [DataField]
+ public bool AutoStand = true;
/// <summary>
/// Fixture we track for the collision.
--- /dev/null
+using Content.Shared.Stunnable;
+
+namespace Content.Server.Stunnable;
+
+public sealed class StunSystem : SharedStunSystem;
+
private void TryDoCollideStun(EntityUid uid, StunOnCollideComponent component, EntityUid target)
{
+ if (!TryComp<StatusEffectsComponent>(target, out var status))
+ return;
- if (TryComp<StatusEffectsComponent>(target, out var status))
- {
- _stunSystem.TryStun(target, TimeSpan.FromSeconds(component.StunAmount), true, status);
+ _stunSystem.TryStun(target, component.StunAmount, component.Refresh, status);
- _stunSystem.TryKnockdown(target, TimeSpan.FromSeconds(component.KnockdownAmount), true,
- status);
+ _stunSystem.TryKnockdown(target, component.KnockdownAmount, component.Refresh, component.AutoStand);
- _stunSystem.TrySlowdown(target, TimeSpan.FromSeconds(component.SlowdownAmount), true,
- component.WalkSpeedMultiplier, component.RunSpeedMultiplier, status);
- }
+ _stunSystem.TrySlowdown(target, component.SlowdownAmount, component.Refresh, component.WalkSpeedModifier, component.SprintSpeedModifier, status);
}
private void HandleCollide(EntityUid uid, StunOnCollideComponent component, ref StartCollideEvent args)
{
+++ /dev/null
-using Content.Shared.Stunnable;
-
-namespace Content.Server.Stunnable
-{
- public sealed class StunSystem : SharedStunSystem
- {}
-}
{
// Expiring status effects would remove the components needed for sleeping
_statusEffectOld.TryRemoveStatusEffect(ent.Owner, "Stun");
- _statusEffectOld.TryRemoveStatusEffect(ent.Owner, "KnockedDown");
EnsureComp<StunnedComponent>(ent);
EnsureComp<KnockedDownComponent>(ent);
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float StunBonus = 2f;
+ /// <summary>
+ /// Modifier for the amount of time it takes an entity to stand up if cuffed.
+ /// </summary>
+ [DataField]
+ public float StandupMod = 5f;
+
+ /// <summary>
+ /// Modifier to the speed of an entity who is cuffed, does not stack with KnockedMovementMod
+ /// </summary>
+ [DataField]
+ public float MovementMod = 1f;
+
+ /// <summary>
+ /// Modifier to the knocked down speed of an entity who is cuffed
+ /// </summary>
+ [DataField]
+ public float KnockedMovementMod = 0.4f;
+
/// <summary>
/// Will the cuffs break when removed?
/// </summary>
using Content.Shared.Item;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Events;
+using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Pulling.Events;
using Content.Shared.Rejuvenate;
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
+ [Dependency] private readonly MovementSpeedModifierSystem _move = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
+ private EntityQuery<HandcuffComponent> _cuffQuery;
+
public override void Initialize()
{
base.Initialize();
+ _cuffQuery = GetEntityQuery<HandcuffComponent>();
+
SubscribeLocalEvent<CuffableComponent, HandCountChangedEvent>(OnHandCountChanged);
SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
SubscribeLocalEvent<HandcuffComponent, AddCuffDoAfterEvent>(OnAddCuffDoAfter);
SubscribeLocalEvent<HandcuffComponent, VirtualItemDeletedEvent>(OnCuffVirtualItemDeleted);
+ SubscribeLocalEvent<CuffableComponent, GetStandUpTimeEvent>(OnCuffableStandupArgs);
+ SubscribeLocalEvent<CuffableComponent, KnockedDownRefreshEvent>(OnCuffableKnockdownRefresh);
+ SubscribeLocalEvent<CuffableComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
}
private void CheckInteract(Entity<CuffableComponent> ent, ref InteractionAttemptEvent args)
_adminLog.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(user):player} has cuffed {ToPrettyString(target):player}");
}
+
+ if (!MathHelper.CloseTo(component.MovementMod, 1f))
+ _move.RefreshMovementSpeedModifiers(target);
}
else
{
}
}
+ /// <summary>
+ /// Takes longer to stand up when cuffed
+ /// </summary>
+ private void OnCuffableStandupArgs(Entity<CuffableComponent> ent, ref GetStandUpTimeEvent time)
+ {
+ if (!HasComp<KnockedDownComponent>(ent) || !IsCuffed(ent))
+ return;
+
+ var cuffs = GetAllCuffs(ent.Comp);
+ var mod = 1f;
+
+ if (cuffs.Count == 0)
+ return;
+
+ foreach (var cuff in cuffs)
+ {
+ if (!_cuffQuery.TryComp(cuff, out var comp))
+ continue;
+
+ // Get the worst modifier
+ mod = Math.Max(mod, comp.StandupMod);
+ }
+
+ time.DoAfterTime *= mod;
+ }
+
+ private void OnCuffableKnockdownRefresh(Entity<CuffableComponent> ent, ref KnockedDownRefreshEvent args)
+ {
+ var cuffs = GetAllCuffs(ent.Comp);
+ var mod = 1f;
+
+ if (cuffs.Count == 0)
+ return;
+
+ foreach (var cuff in cuffs)
+ {
+ if (!_cuffQuery.TryComp(cuff, out var comp))
+ continue;
+
+ // Get the worst modifier
+ mod = Math.Min(mod, comp.KnockedMovementMod);
+ }
+
+ args.SpeedModifier *= mod;
+ }
+
+ private void OnRefreshMovementSpeedModifiers(Entity<CuffableComponent> ent, ref RefreshMovementSpeedModifiersEvent args)
+ {
+ var cuffs = GetAllCuffs(ent.Comp);
+ var mod = 1f;
+
+ if (cuffs.Count == 0)
+ return;
+
+ foreach (var cuff in cuffs)
+ {
+ if (!_cuffQuery.TryComp(cuff, out var comp))
+ continue;
+
+ // Get the worst modifier
+ mod = Math.Min(mod, comp.MovementMod);
+ }
+
+ args.ModifySpeed(mod);
+ }
+
/// <summary>
/// Adds virtual cuff items to the user's hands.
/// </summary>
shoved = true;
}
+ if (!MathHelper.CloseTo(cuff.MovementMod, 1f))
+ _move.RefreshMovementSpeedModifiers(target);
+
if (cuffable.CuffedHandCount == 0)
{
if (user != null)
using System.Numerics;
using Content.Shared.Alert;
using Content.Shared.FixedPoint;
+using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
public float StaminaDamage;
/// <summary>
- /// How much stamina damage is required to entire stam crit.
+ /// How much stamina damage is required to enter stam crit.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
public float CritThreshold = 100f;
[DataField, AutoNetworkedField]
public float AfterCritDecayMultiplier = 5f;
+ /// <summary>
+ /// This is how much stamina damage a mob takes when it forces itself to stand up before modifiers
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float ForceStandStamina = 10f;
+
+ /// <summary>
+ /// What sound should play when we successfully stand up
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier ForceStandSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
+
/// <summary>
/// Thresholds that determine an entity's slowdown as a function of stamina damage.
/// </summary>
/// <summary>
/// Tries to take stamina damage without raising the entity over the crit threshold.
/// </summary>
- public bool TryTakeStamina(EntityUid uid, float value, StaminaComponent? component = null, EntityUid? source = null, EntityUid? with = null)
+ public bool TryTakeStamina(EntityUid uid, float value, StaminaComponent? component = null, EntityUid? source = null, EntityUid? with = null, bool visual = false)
{
// Something that has no Stamina component automatically passes stamina checks
if (!Resolve(uid, ref component, false))
var oldStam = component.StaminaDamage;
- if (oldStam + value > component.CritThreshold || component.Critical)
+ if (oldStam + value >= component.CritThreshold || component.Critical)
return false;
- TakeStaminaDamage(uid, value, component, source, with, visual: false);
+ TakeStaminaDamage(uid, value, component, source, with, visual: visual);
return true;
}
--- /dev/null
+using Content.Shared.Hands.Components;
+using Content.Shared.Stunnable;
+
+namespace Content.Shared.Hands.EntitySystems;
+
+/// <summary>
+/// This is for events that don't affect normal hand functions but do care about hands.
+/// </summary>
+public abstract partial class SharedHandsSystem
+{
+ private void InitializeEventListeners()
+ {
+ SubscribeLocalEvent<HandsComponent, GetStandUpTimeEvent>(OnStandupArgs);
+ }
+
+ /// <summary>
+ /// Reduces the time it takes to stand up based on the number of hands we have available.
+ /// </summary>
+ private void OnStandupArgs(Entity<HandsComponent> ent, ref GetStandUpTimeEvent time)
+ {
+ if (!HasComp<KnockedDownComponent>(ent))
+ return;
+
+ var hands = GetEmptyHandCount(ent.Owner);
+
+ if (hands == 0)
+ return;
+
+ time.DoAfterTime *= (float)ent.Comp.Count / (hands + ent.Comp.Count);
+ }
+}
InitializeDrop();
InitializePickup();
InitializeRelay();
+ InitializeEventListeners();
SubscribeLocalEvent<HandsComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<HandsComponent, MapInitEvent>(OnMapInit);
return false;
}
+ /// <summary>
+ /// Does this entity have any empty hands, and how many?
+ /// </summary>
+ public int GetEmptyHandCount(Entity<HandsComponent?> entity)
+ {
+ if (!Resolve(entity, ref entity.Comp, false) || entity.Comp.Count == 0)
+ return 0;
+
+ var hands = 0;
+
+ foreach (var hand in EnumerateHands(entity))
+ {
+ if (!HandIsEmpty(entity, hand))
+ continue;
+ hands++;
+ }
+
+ return hands;
+ }
+
/// <summary>
/// Attempts to retrieve the item held in the entity's active hand.
/// </summary>
[KeyFunctions]
public static class ContentKeyFunctions
{
+ public static readonly BoundKeyFunction ToggleKnockdown = "ToggleKnockdown";
public static readonly BoundKeyFunction UseItemInHand = "ActivateItemInHand";
public static readonly BoundKeyFunction AltUseItemInHand = "AltActivateItemInHand";
public static readonly BoundKeyFunction ActivateItemInWorld = "ActivateItemInWorld";
--- /dev/null
+using Content.Shared.Movement.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Movement.Components;
+
+/// <summary>
+/// This is used to apply a friction modifier to an entity temporarily
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(MovementModStatusSystem))]
+public sealed partial class FrictionStatusEffectComponent : Component
+{
+ /// <summary>
+ /// Friction modifier applied as a status.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float FrictionModifier = 1f;
+
+ /// <summary>
+ /// Acceleration modifier applied as a status.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float AccelerationModifier = 1f;
+}
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Movement.Components;
+
+/// <summary>
+/// This component ensures an entity is always in the KnockedDown State and cannot stand. Great for any entities you
+/// don't want to collide with other mobs, don't want eating projectiles and don't want to get knocked down.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class WormComponent : Component
+{
+ /// <summary>
+ /// Modifier for KnockedDown Friction, or in this components case, all friction
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float FrictionModifier = 1f;
+
+ /// <summary>
+ /// Modifier for KnockedDown Movement, or in this components case, all movement
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float SpeedModifier = 1f;
+}
--- /dev/null
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Events;
+using Content.Shared.StatusEffectNew;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Movement.Systems;
+
+/// <summary>
+/// This handles the application of movement and friction modifiers to an entity as status effects.
+/// </summary>
+public sealed class MovementModStatusSystem : EntitySystem
+{
+ public static readonly EntProtoId StatusEffectFriction = "StatusEffectFriction";
+
+ [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
+ [Dependency] private readonly StatusEffectsSystem _status = default!;
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<FrictionStatusEffectComponent, StatusEffectRemovedEvent>(OnFrictionStatusEffectRemoved);
+ SubscribeLocalEvent<FrictionStatusEffectComponent, StatusEffectRelayedEvent<RefreshFrictionModifiersEvent>>(OnRefreshFrictionStatus);
+ SubscribeLocalEvent<FrictionStatusEffectComponent, StatusEffectRelayedEvent<TileFrictionEvent>>(OnRefreshTileFrictionStatus);
+ }
+
+ private void OnRefreshFrictionStatus(Entity<FrictionStatusEffectComponent> ent, ref StatusEffectRelayedEvent<RefreshFrictionModifiersEvent> args)
+ {
+ var ev = args.Args;
+ ev.ModifyFriction(ent.Comp.FrictionModifier);
+ ev.ModifyAcceleration(ent.Comp.AccelerationModifier);
+ args.Args = ev;
+ }
+
+ private void OnRefreshTileFrictionStatus(Entity<FrictionStatusEffectComponent> ent, ref StatusEffectRelayedEvent<TileFrictionEvent> args)
+ {
+ var ev = args.Args;
+ ev.Modifier *= ent.Comp.FrictionModifier;
+ args.Args = ev;
+ }
+
+ /// <summary>
+ /// Applies a friction de-buff to the player.
+ /// </summary>
+ public bool TryFriction(EntityUid uid,
+ TimeSpan time,
+ bool refresh,
+ float friction,
+ float acceleration)
+ {
+ if (time <= TimeSpan.Zero)
+ return false;
+
+ if (refresh)
+ {
+ return _status.TryUpdateStatusEffectDuration(uid, StatusEffectFriction, out var status, time)
+ && TrySetFrictionStatus(status.Value, friction, acceleration, uid);
+ }
+ else
+ {
+ return _status.TryAddStatusEffectDuration(uid, StatusEffectFriction, out var status, time)
+ && TrySetFrictionStatus(status.Value, friction, acceleration, uid);
+ }
+ }
+
+ /// <summary>
+ /// Sets the friction status modifiers for a status effect.
+ /// </summary>
+ /// <param name="status">The status effect entity we're modifying.</param>
+ /// <param name="friction">The friction modifier we're applying.</param>
+ /// <param name="entity">The entity the status effect is attached to that we need to refresh.</param>
+ private bool TrySetFrictionStatus(Entity<FrictionStatusEffectComponent?> status, float friction, EntityUid entity)
+ {
+ return TrySetFrictionStatus(status, friction, friction, entity);
+ }
+
+ /// <summary>
+ /// Sets the friction status modifiers for a status effect.
+ /// </summary>
+ /// <param name="status">The status effect entity we're modifying.</param>
+ /// <param name="friction">The friction modifier we're applying.</param>
+ /// <param name="acceleration">The acceleration modifier we're applying</param>
+ /// <param name="entity">The entity the status effect is attached to that we need to refresh.</param>
+ private bool TrySetFrictionStatus(Entity<FrictionStatusEffectComponent?> status, float friction, float acceleration, EntityUid entity)
+ {
+ if (!Resolve(status, ref status.Comp, false))
+ return false;
+
+ status.Comp.FrictionModifier = friction;
+ status.Comp.AccelerationModifier = acceleration;
+ Dirty(status);
+
+ _movementSpeedModifier.RefreshFrictionModifiers(entity);
+ return true;
+ }
+
+ private void OnFrictionStatusEffectRemoved(Entity<FrictionStatusEffectComponent> entity, ref StatusEffectRemovedEvent args)
+ {
+ TrySetFrictionStatus(entity!, 1f, args.Target);
+ }
+}
-using System.Text.Json.Serialization.Metadata;
using Content.Shared.CCVar;
using Content.Shared.Inventory;
using Content.Shared.Movement.Components;
-using Content.Shared.Movement.Events;
+using Content.Shared.Standing;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
public sealed class MovementSpeedModifierSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly IConfigurationManager _configManager = default!;
+ [Dependency] private readonly IConfigurationManager _configManager = default!;
private float _frictionModifier;
private float _airDamping;
{
base.Initialize();
SubscribeLocalEvent<MovementSpeedModifierComponent, MapInitEvent>(OnModMapInit);
+ SubscribeLocalEvent<MovementSpeedModifierComponent, DownedEvent>(OnDowned);
+ SubscribeLocalEvent<MovementSpeedModifierComponent, StoodEvent>(OnStand);
Subs.CVar(_configManager, CCVars.TileFrictionModifier, value => _frictionModifier = value, true);
Subs.CVar(_configManager, CCVars.AirFriction, value => _airDamping = value, true);
Dirty(ent);
}
+ private void OnDowned(Entity<MovementSpeedModifierComponent> entity, ref DownedEvent args)
+ {
+ RefreshFrictionModifiers(entity);
+ RefreshMovementSpeedModifiers(entity);
+ }
+
+ private void OnStand(Entity<MovementSpeedModifierComponent> entity, ref StoodEvent args)
+ {
+ RefreshFrictionModifiers(entity);
+ RefreshMovementSpeedModifiers(entity);
+ }
+
public void RefreshWeightlessModifiers(EntityUid uid, MovementSpeedModifierComponent? move = null)
{
if (!Resolve(uid, ref move, false))
--- /dev/null
+using Content.Shared.Alert;
+using Content.Shared.Movement.Components;
+using Content.Shared.Popups;
+using Content.Shared.Rejuvenate;
+using Content.Shared.Stunnable;
+
+namespace Content.Shared.Movement.Systems;
+
+/// <summary>
+/// This handles the worm component
+/// </summary>
+public sealed class WormSystem : EntitySystem
+{
+ [Dependency] private readonly AlertsSystem _alerts = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedStunSystem _stun = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<WormComponent, StandUpAttemptEvent>(OnStandAttempt);
+ SubscribeLocalEvent<WormComponent, KnockedDownRefreshEvent>(OnKnockedDownRefresh);
+ SubscribeLocalEvent<WormComponent, RejuvenateEvent>(OnRejuvenate);
+ SubscribeLocalEvent<WormComponent, MapInitEvent>(OnMapInit);
+ }
+
+ private void OnMapInit(Entity<WormComponent> ent, ref MapInitEvent args)
+ {
+ EnsureComp<KnockedDownComponent>(ent, out var knocked);
+ _alerts.ShowAlert(ent, SharedStunSystem.KnockdownAlert);
+ _stun.SetAutoStand((ent, knocked));
+ }
+
+ private void OnRejuvenate(Entity<WormComponent> ent, ref RejuvenateEvent args)
+ {
+ RemComp<WormComponent>(ent);
+ }
+
+ private void OnStandAttempt(Entity<WormComponent> ent, ref StandUpAttemptEvent args)
+ {
+ if (args.Cancelled)
+ return;
+
+ args.Cancelled = true;
+ args.Message = (Loc.GetString("worm-component-stand-attempt"), PopupType.SmallCaution);
+ args.Autostand = false;
+ }
+
+ private void OnKnockedDownRefresh(Entity<WormComponent> ent, ref KnockedDownRefreshEvent args)
+ {
+ args.FrictionModifier *= ent.Comp.FrictionModifier;
+ args.SpeedModifier *= ent.Comp.SpeedModifier;
+ }
+}
public sealed partial class SlidingComponent : Component
{
/// <summary>
- /// A list of SuperSlippery entities the entity with this component is colliding with.
+ /// The friction modifier that will be applied to any friction calculations.
/// </summary>
[DataField, AutoNetworkedField]
- public HashSet<EntityUid> CollidingEntities = new ();
+ public float FrictionModifier;
/// <summary>
- /// The friction modifier that will be applied to any friction calculations.
+ /// Hashset of contacting entities.
/// </summary>
- [DataField, AutoNetworkedField]
- public float FrictionModifier;
+ [DataField]
+ public HashSet<EntityUid> Contacting = new();
}
-using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Systems;
using Content.Shared.Standing;
-using Content.Shared.Stunnable;
+using Content.Shared.Throwing;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
namespace Content.Shared.Slippery;
public sealed class SlidingSystem : EntitySystem
{
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+ [Dependency] private readonly MovementSpeedModifierSystem _speedModifierSystem = default!;
+
+ private EntityQuery<SlipperyComponent> _slipperyQuery;
+
public override void Initialize()
{
base.Initialize();
+ _slipperyQuery = GetEntityQuery<SlipperyComponent>();
+
+ SubscribeLocalEvent<SlidingComponent, ComponentInit>(OnComponentInit);
+ SubscribeLocalEvent<SlidingComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<SlidingComponent, StoodEvent>(OnStand);
SubscribeLocalEvent<SlidingComponent, StartCollideEvent>(OnStartCollide);
SubscribeLocalEvent<SlidingComponent, EndCollideEvent>(OnEndCollide);
+ SubscribeLocalEvent<SlidingComponent, RefreshFrictionModifiersEvent>(OnRefreshFrictionModifiers);
+ SubscribeLocalEvent<SlidingComponent, ThrowerImpulseEvent>(OnThrowerImpulse);
+ SubscribeLocalEvent<SlidingComponent, ShooterImpulseEvent>(ShooterImpulseEvent);
+ }
+
+ /// <summary>
+ /// When the component is first added, calculate the friction modifier we need.
+ /// Don't do this more than once to avoid mispredicts.
+ /// </summary>
+ private void OnComponentInit(Entity<SlidingComponent> entity, ref ComponentInit args)
+ {
+ if (CalculateSlidingModifier(entity))
+ _speedModifierSystem.RefreshFrictionModifiers(entity);
+ }
+
+ /// <summary>
+ /// When the component is removed, refresh friction modifiers and set ours to 1 to avoid causing issues.
+ /// </summary>
+ private void OnComponentShutdown(Entity<SlidingComponent> entity, ref ComponentShutdown args)
+ {
+ entity.Comp.FrictionModifier = 1;
+ _speedModifierSystem.RefreshFrictionModifiers(entity);
}
/// <summary>
}
/// <summary>
- /// Sets friction to 0 if colliding with a SuperSlippery Entity.
+ /// Updates friction when we collide with a slippery entity
/// </summary>
- private void OnStartCollide(EntityUid uid, SlidingComponent component, ref StartCollideEvent args)
+ private void OnStartCollide(Entity<SlidingComponent> entity, ref StartCollideEvent args)
{
- if (!TryComp<SlipperyComponent>(args.OtherEntity, out var slippery) || !slippery.SlipData.SuperSlippery)
+ if (!_slipperyQuery.TryComp(args.OtherEntity, out var slippery) || !slippery.AffectsSliding)
return;
- component.CollidingEntities.Add(args.OtherEntity);
- Dirty(uid, component);
+ CalculateSlidingModifier(entity);
+ _speedModifierSystem.RefreshFrictionModifiers(entity);
}
/// <summary>
- /// Set friction to normal when ending collision with a SuperSlippery entity.
+ /// Update friction when we stop colliding with a slippery entity
/// </summary>
- private void OnEndCollide(EntityUid uid, SlidingComponent component, ref EndCollideEvent args)
+ private void OnEndCollide(Entity<SlidingComponent> entity, ref EndCollideEvent args)
{
- if (!component.CollidingEntities.Remove(args.OtherEntity))
+ if (!_slipperyQuery.TryComp(args.OtherEntity, out var slippery) || !slippery.AffectsSliding)
return;
- if (component.CollidingEntities.Count == 0)
- RemComp<SlidingComponent>(uid);
+ if (!CalculateSlidingModifier(entity, args.OtherEntity))
+ {
+ RemComp<SlidingComponent>(entity);
+ return;
+ }
- Dirty(uid, component);
+ _speedModifierSystem.RefreshFrictionModifiers(entity);
+ }
+
+ /// <summary>
+ /// Gets contacting slippery entities and averages their friction modifiers.
+ /// </summary>
+ private bool CalculateSlidingModifier(Entity<SlidingComponent, PhysicsComponent?> entity, EntityUid? ignore = null)
+ {
+ if (!Resolve(entity, ref entity.Comp2, false))
+ return false;
+
+ var friction = 0.0f;
+ var count = 0;
+ entity.Comp1.Contacting.Clear();
+
+ _physics.GetContactingEntities((entity, entity.Comp2), entity.Comp1.Contacting);
+
+ foreach (var ent in entity.Comp1.Contacting)
+ {
+ if (ent == ignore || !_slipperyQuery.TryComp(ent, out var slippery) || !slippery.AffectsSliding)
+ continue;
+
+ friction += slippery.SlipData.SlipFriction;
+
+ count++;
+ }
+
+ if (count > 0)
+ {
+ entity.Comp1.FrictionModifier = friction / count;
+ Dirty(entity.Owner, entity.Comp1);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void OnRefreshFrictionModifiers(Entity<SlidingComponent> entity, ref RefreshFrictionModifiersEvent args)
+ {
+ args.ModifyFriction(entity.Comp.FrictionModifier);
+ args.ModifyAcceleration(entity.Comp.FrictionModifier);
+ }
+
+ private void OnThrowerImpulse(Entity<SlidingComponent> entity, ref ThrowerImpulseEvent args)
+ {
+ args.Push = true;
+ }
+
+ private void ShooterImpulseEvent(Entity<SlidingComponent> entity, ref ShooterImpulseEvent args)
+ {
+ args.Push = true;
}
}
[Access(Other = AccessPermissions.ReadWriteExecute)]
public SoundSpecifier SlipSound = new SoundPathSpecifier("/Audio/Effects/slip.ogg");
+ /// <summary>
+ /// Should this component's friction factor into sliding friction?
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public bool AffectsSliding;
+
+ /// <summary>
+ /// How long should this component apply the FrictionStatusComponent?
+ /// Note: This does stack with SlidingComponent since they are two separate Components
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public TimeSpan FrictionStatusTime = TimeSpan.FromSeconds(0.5f);
+
+ /// <summary>
+ /// How much stamina damage should this component do on slip?
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float StaminaDamage = 25f;
+
/// <summary>
/// Loads the data needed to determine how slippery something is.
/// </summary>
public sealed partial class SlipperyEffectEntry
{
/// <summary>
- /// How many seconds the mob will be paralyzed for.
+ /// How many seconds the mob will be stunned for.
+ /// </summary>
+ [DataField]
+ public TimeSpan StunTime = TimeSpan.FromSeconds(0.5);
+
+ /// <summary>
+ /// How many seconds the mob will be knocked down for.
+ /// </summary>
+ [DataField]
+ public TimeSpan KnockdownTime = TimeSpan.FromSeconds(1.5);
+
+ /// <summary>
+ /// Should the slipped entity try to stand up when Knockdown ends?
/// </summary>
[DataField]
- public TimeSpan ParalyzeTime = TimeSpan.FromSeconds(1.5);
+ public bool AutoStand = true;
/// <summary>
/// The entity's speed will be multiplied by this to slip it forwards.
/// This is used to store the friction modifier that is used on a sliding entity.
/// </summary>
[DataField]
- public float SlipFriction;
+ public float SlipFriction = 0.5f;
}
}
using Content.Shared.Administration.Logs;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Inventory;
using Robust.Shared.Network;
public sealed class SlipperySystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly MovementModStatusSystem _movementMod = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
+ [Dependency] private readonly SharedStaminaSystem _stamina = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
{
_physics.SetLinearVelocity(other, physics.LinearVelocity * component.SlipData.LaunchForwardsMultiplier, body: physics);
- if (component.SlipData.SuperSlippery && requiresContact)
- {
- var sliding = EnsureComp<SlidingComponent>(other);
- sliding.CollidingEntities.Add(uid);
- // Why the fuck does this assertion stack overflow every once in a while
- DebugTools.Assert(_physics.GetContactingEntities(other, physics).Contains(uid));
- }
+ if (component.AffectsSliding && requiresContact)
+ EnsureComp<SlidingComponent>(other);
}
- var playSound = !_statusEffects.HasStatusEffect(other, "KnockedDown");
-
- _stun.TryParalyze(other, component.SlipData.ParalyzeTime, true);
-
- // Preventing from playing the slip sound when you are already knocked down.
- if (playSound)
+ // Preventing from playing the slip sound and stunning when you are already knocked down.
+ if (!HasComp<KnockedDownComponent>(other))
{
+ _stun.TryStun(other, component.SlipData.StunTime, true);
+ _stamina.TakeStaminaDamage(other, component.StaminaDamage); // Note that this can stamCrit
+ _movementMod.TryFriction(other, component.FrictionStatusTime, true, component.SlipData.SlipFriction, component.SlipData.SlipFriction);
_audio.PlayPredicted(component.SlipSound, other, other);
}
+ _stun.TryKnockdown(other, component.SlipData.KnockdownTime, true, true);
- _adminLogger.Add(LogType.Slip, LogImpact.Low,
- $"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(uid):entity}");
+ _adminLogger.Add(LogType.Slip, LogImpact.Low, $"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(uid):entity}");
}
}
[DataField, AutoNetworkedField]
public bool Standing { get; set; } = true;
+ /// <summary>
+ /// Time it takes us to stand up
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public TimeSpan StandTime = TimeSpan.FromSeconds(2);
+
+ /// <summary>
+ /// Default Friction modifier for knocked down players.
+ /// Makes them accelerate and deccelerate slower.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float FrictionModifier = 0.4f;
+
+ /// <summary>
+ /// Base modifier to the maximum movement speed of a knocked down mover.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float SpeedModifier = 0.3f;
+
/// <summary>
/// List of fixtures that had their collision mask changed when the entity was downed.
/// Required for re-adding the collision mask.
using Content.Shared.Hands.Components;
+using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Physics;
using Content.Shared.Rotation;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
// If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited.
- private const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable;
+ public const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StandingStateComponent, AttemptMobCollideEvent>(OnMobCollide);
SubscribeLocalEvent<StandingStateComponent, AttemptMobTargetCollideEvent>(OnMobTargetCollide);
+ SubscribeLocalEvent<StandingStateComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
+ SubscribeLocalEvent<StandingStateComponent, RefreshFrictionModifiersEvent>(OnRefreshFrictionModifiers);
+ SubscribeLocalEvent<StandingStateComponent, TileFrictionEvent>(OnTileFriction);
}
private void OnMobTargetCollide(Entity<StandingStateComponent> ent, ref AttemptMobTargetCollideEvent args)
}
}
+ private void OnRefreshMovementSpeedModifiers(Entity<StandingStateComponent> entity, ref RefreshMovementSpeedModifiersEvent args)
+ {
+ if (!entity.Comp.Standing)
+ args.ModifySpeed(entity.Comp.FrictionModifier);
+ }
+
+ private void OnRefreshFrictionModifiers(Entity<StandingStateComponent> entity, ref RefreshFrictionModifiersEvent args)
+ {
+ if (entity.Comp.Standing)
+ return;
+
+ args.ModifyFriction(entity.Comp.FrictionModifier);
+ args.ModifyAcceleration(entity.Comp.FrictionModifier);
+ }
+
+ private void OnTileFriction(Entity<StandingStateComponent> entity, ref TileFrictionEvent args)
+ {
+ if (!entity.Comp.Standing)
+ args.Modifier *= entity.Comp.FrictionModifier;
+ }
+
public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null)
{
if (!Resolve(uid, ref standingState, false))
+using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Systems;
using Content.Shared.StatusEffectNew.Components;
using Robust.Shared.Player;
{
SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerAttachedEvent>(RelayStatusEffectEvent);
SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerDetachedEvent>(RelayStatusEffectEvent);
+
+ SubscribeLocalEvent<StatusEffectContainerComponent, RefreshFrictionModifiersEvent>(RefRelayStatusEffectEvent);
+ SubscribeLocalEvent<StatusEffectContainerComponent, TileFrictionEvent>(RefRelayStatusEffectEvent);
}
private void RefRelayStatusEffectEvent<T>(EntityUid uid, StatusEffectContainerComponent component, ref T args) where T : struct
-using Robust.Shared.Audio;
+using Content.Shared.DoAfter;
using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Stunnable;
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStunSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas:true), AutoGenerateComponentPause, Access(typeof(SharedStunSystem))]
public sealed partial class KnockedDownComponent : Component
{
- [DataField("helpInterval"), AutoNetworkedField]
- public float HelpInterval = 1f;
+ /// <summary>
+ /// Game time that we can stand up.
+ /// </summary>
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
+ public TimeSpan NextUpdate;
- [DataField("helpAttemptSound")]
- public SoundSpecifier StunAttemptSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
+ /// <summary>
+ /// Should we try to stand up?
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public bool AutoStand = true;
- [ViewVariables, AutoNetworkedField]
- public float HelpTimer = 0f;
+ /// <summary>
+ /// The Standing Up DoAfter.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public ushort? DoAfterId;
+
+ /// <summary>
+ /// Friction modifier for knocked down players.
+ /// Makes them accelerate and deccelerate slower.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float FrictionModifier = 1f; // Should add a friction modifier to slipping to compensate for this
+
+ /// <summary>
+ /// Modifier to the maximum movement speed of a knocked down mover.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float SpeedModifier = 1f;
+
+ /// <summary>
+ /// How long does it take us to get up?
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public TimeSpan GetUpDoAfter = TimeSpan.FromSeconds(1);
}
--- /dev/null
+using Content.Shared.Alert;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Hands;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Input;
+using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Rejuvenate;
+using Content.Shared.Standing;
+using Robust.Shared.Audio;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Stunnable;
+
+/// <summary>
+/// This contains the knockdown logic for the stun system for organization purposes.
+/// </summary>
+public abstract partial class SharedStunSystem
+{
+ // TODO: Both of these constants need to be moved to a component somewhere, and need to be tweaked for balance...
+ // We don't always have standing state available when these are called so it can't go there
+ // Maybe I can pass the values to KnockedDownComponent from Standing state on Component init?
+ // Default knockdown timer
+ public static readonly TimeSpan DefaultKnockedDuration = TimeSpan.FromSeconds(0.5f);
+ // Minimum damage taken to refresh our knockdown timer to the default duration
+ public static readonly float KnockdownDamageThreshold = 5f;
+
+ [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+ [Dependency] private readonly StandingStateSystem _standingState = default!;
+
+ public static readonly ProtoId<AlertPrototype> KnockdownAlert = "Knockdown";
+
+ private void InitializeKnockdown()
+ {
+ SubscribeLocalEvent<KnockedDownComponent, RejuvenateEvent>(OnRejuvenate);
+
+ // Startup and Shutdown
+ SubscribeLocalEvent<KnockedDownComponent, ComponentInit>(OnKnockInit);
+ SubscribeLocalEvent<KnockedDownComponent, ComponentShutdown>(OnKnockShutdown);
+
+ // Action blockers
+ SubscribeLocalEvent<KnockedDownComponent, BuckleAttemptEvent>(OnBuckleAttempt);
+ SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandUpAttempt);
+
+ // Updating movement a friction
+ SubscribeLocalEvent<KnockedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshKnockedSpeed);
+ SubscribeLocalEvent<KnockedDownComponent, RefreshFrictionModifiersEvent>(OnRefreshFriction);
+ SubscribeLocalEvent<KnockedDownComponent, TileFrictionEvent>(OnKnockedTileFriction);
+ SubscribeLocalEvent<KnockedDownComponent, DidEquipHandEvent>(OnHandEquipped);
+ SubscribeLocalEvent<KnockedDownComponent, DidUnequipHandEvent>(OnHandUnequipped);
+
+ // DoAfter event subscriptions
+ SubscribeLocalEvent<KnockedDownComponent, TryStandDoAfterEvent>(OnStandDoAfter);
+
+ // Knockdown Extenders
+ SubscribeLocalEvent<KnockedDownComponent, DamageChangedEvent>(OnDamaged);
+
+ // Handling Alternative Inputs
+ SubscribeAllEvent<ForceStandUpEvent>(OnForceStandup);
+ SubscribeLocalEvent<KnockedDownComponent, KnockedDownAlertEvent>(OnKnockedDownAlert);
+
+ CommandBinds.Builder
+ .Bind(ContentKeyFunctions.ToggleKnockdown, InputCmdHandler.FromDelegate(HandleToggleKnockdown, handle: false))
+ .Register<SharedStunSystem>();
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator<KnockedDownComponent>();
+
+ while (query.MoveNext(out var uid, out var knockedDown))
+ {
+ if (!knockedDown.AutoStand || knockedDown.DoAfterId.HasValue || knockedDown.NextUpdate > GameTiming.CurTime)
+ continue;
+
+ TryStanding(uid, out knockedDown.DoAfterId);
+ DirtyField(uid, knockedDown, nameof(KnockedDownComponent.DoAfterId));
+ }
+ }
+
+ private void OnRejuvenate(Entity<KnockedDownComponent> entity, ref RejuvenateEvent args)
+ {
+ SetKnockdownTime(entity, GameTiming.CurTime);
+
+ if (entity.Comp.AutoStand)
+ RemComp<KnockedDownComponent>(entity);
+ }
+
+ #region Startup and Shutdown
+
+ private void OnKnockInit(Entity<KnockedDownComponent> entity, ref ComponentInit args)
+ {
+ // Other systems should handle dropping held items...
+ _standingState.Down(entity, true, false);
+ RefreshKnockedMovement(entity);
+ }
+
+ private void OnKnockShutdown(Entity<KnockedDownComponent> entity, ref ComponentShutdown args)
+ {
+ // This is jank but if we don't do this it'll still use the knockedDownComponent modifiers for friction because it hasn't been deleted quite yet.
+ entity.Comp.FrictionModifier = 1f;
+ entity.Comp.SpeedModifier = 1f;
+
+ _standingState.Stand(entity);
+ Alerts.ClearAlert(entity, KnockdownAlert);
+ }
+
+ #endregion
+
+ #region API
+
+ /// <summary>
+ /// Sets the autostand property of a <see cref="KnockedDownComponent"/> on an entity to true or false and dirties it.
+ /// Defaults to false.
+ /// </summary>
+ /// <param name="entity">Entity we want to edit the data field of.</param>
+ /// <param name="autoStand">What we want to set the data field to.</param>
+ public void SetAutoStand(Entity<KnockedDownComponent?> entity, bool autoStand = false)
+ {
+ if (!Resolve(entity, ref entity.Comp, false))
+ return;
+
+ entity.Comp.AutoStand = autoStand;
+ DirtyField(entity, entity.Comp, nameof(entity.Comp.AutoStand));
+ }
+
+ /// <summary>
+ /// Cancels the DoAfter of an entity with the <see cref="KnockedDownComponent"/> who is trying to stand.
+ /// </summary>
+ /// <param name="entity">Entity who we are canceling the DoAfter for.</param>
+ public void CancelKnockdownDoAfter(Entity<KnockedDownComponent?> entity)
+ {
+ if (!Resolve(entity, ref entity.Comp, false))
+ return;
+
+ if (entity.Comp.DoAfterId == null)
+ return;
+
+ DoAfter.Cancel(entity.Owner, entity.Comp.DoAfterId.Value);
+ entity.Comp.DoAfterId = null;
+ DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.DoAfterId));
+ }
+
+ /// <summary>
+ /// Updates the knockdown timer of a knocked down entity with a given inputted time, then dirties the time.
+ /// </summary>
+ /// <param name="entity">Entity who's knockdown time we're updating.</param>
+ /// <param name="time">The time we're updating with.</param>
+ /// <param name="refresh">Whether we're resetting the timer or adding to the current timer.</param>
+ public void UpdateKnockdownTime(Entity<KnockedDownComponent> entity, TimeSpan time, bool refresh = true)
+ {
+ if (refresh)
+ RefreshKnockdownTime(entity, time);
+ else
+ AddKnockdownTime(entity, time);
+ }
+
+ /// <summary>
+ /// Sets the next update datafield of an entity's <see cref="KnockedDownComponent"/> to a specific time.
+ /// </summary>
+ /// <param name="entity">Entity whose timer we're updating</param>
+ /// <param name="time">The exact time we're setting the next update to.</param>
+ public void SetKnockdownTime(Entity<KnockedDownComponent> entity, TimeSpan time)
+ {
+ entity.Comp.NextUpdate = time;
+ DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.NextUpdate));
+ }
+
+ /// <summary>
+ /// Refreshes the amount of time an entity is knocked down to the inputted time, if it is greater than
+ /// the current time left.
+ /// </summary>
+ /// <param name="entity">Entity whose timer we're updating</param>
+ /// <param name="time">The time we want them to be knocked down for.</param>
+ public void RefreshKnockdownTime(Entity<KnockedDownComponent> entity, TimeSpan time)
+ {
+ var knockedTime = GameTiming.CurTime + time;
+ if (entity.Comp.NextUpdate < knockedTime)
+ SetKnockdownTime(entity, knockedTime);
+ }
+
+ /// <summary>
+ /// Adds our inputted time to an entity's knocked down timer, or sets it to the given time if their timer has expired.
+ /// </summary>
+ /// <param name="entity">Entity whose timer we're updating</param>
+ /// <param name="time">The time we want to add to their knocked down timer.</param>
+ public void AddKnockdownTime(Entity<KnockedDownComponent> entity, TimeSpan time)
+ {
+ if (entity.Comp.NextUpdate < GameTiming.CurTime)
+ {
+ SetKnockdownTime(entity, GameTiming.CurTime + time);
+ return;
+ }
+
+ entity.Comp.NextUpdate += time;
+ DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.NextUpdate));
+ }
+
+ /// <summary>
+ /// Checks if an entity is able to stand, returns true if it can, returns false if it cannot.
+ /// </summary>
+ /// <param name="entity">Entity we're checking</param>
+ /// <returns>Returns whether the entity is able to stand</returns>
+ public bool CanStand(Entity<KnockedDownComponent> entity)
+ {
+ if (entity.Comp.NextUpdate > GameTiming.CurTime)
+ return false;
+
+ if (!Blocker.CanMove(entity))
+ return false;
+
+ var ev = new StandUpAttemptEvent();
+ RaiseLocalEvent(entity, ref ev);
+
+ return !ev.Cancelled;
+ }
+
+ #endregion
+
+ #region Knockdown Logic
+
+ private void HandleToggleKnockdown(ICommonSession? session)
+ {
+ if (session is not { } playerSession)
+ return;
+
+ if (playerSession.AttachedEntity is not { Valid: true } playerEnt || !Exists(playerEnt))
+ return;
+
+ if (!TryComp<KnockedDownComponent>(playerEnt, out var component))
+ {
+ TryKnockdown(playerEnt, DefaultKnockedDuration, true, false, false); // TODO: Unhardcode these numbers
+ return;
+ }
+
+ var stand = !component.DoAfterId.HasValue;
+ SetAutoStand(playerEnt, stand);
+
+ if (stand && TryStanding(playerEnt, out component.DoAfterId))
+ DirtyField(playerEnt, component, nameof(KnockedDownComponent.DoAfterId));
+ else
+ CancelKnockdownDoAfter((playerEnt, component));
+ }
+
+ public bool TryStanding(Entity<KnockedDownComponent?, StandingStateComponent?> entity, out ushort? id)
+ {
+ id = null;
+ // If we aren't knocked down or can't be knocked down, then we did technically succeed in standing up
+ if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2, false))
+ return true;
+
+ id = entity.Comp1.DoAfterId;
+
+ if (!TryStand((entity.Owner, entity.Comp1)))
+ return false;
+
+ var ev = new GetStandUpTimeEvent(entity.Comp2.StandTime);
+ RaiseLocalEvent(entity, ref ev);
+
+ var doAfterArgs = new DoAfterArgs(EntityManager, entity, ev.DoAfterTime, new TryStandDoAfterEvent(), entity, entity)
+ {
+ BreakOnDamage = true,
+ DamageThreshold = 5,
+ CancelDuplicate = true,
+ RequireCanInteract = false,
+ BreakOnHandChange = true
+ };
+
+ // If we try standing don't try standing again
+ if (!DoAfter.TryStartDoAfter(doAfterArgs, out var doAfterId))
+ return false;
+
+ id = doAfterId.Value.Index;
+ return true;
+ }
+
+ /// <summary>
+ /// A variant of <see cref="CanStand"/> used when we're actually trying to stand.
+ /// Main difference is this one affects autostand datafields and also displays popups.
+ /// </summary>
+ /// <param name="entity">Entity we're checking</param>
+ /// <returns>Returns whether the entity is able to stand</returns>
+ public bool TryStand(Entity<KnockedDownComponent> entity)
+ {
+ if (entity.Comp.NextUpdate > GameTiming.CurTime)
+ return false;
+
+ if (!Blocker.CanMove(entity))
+ return false;
+
+ var ev = new StandUpAttemptEvent(entity.Comp.AutoStand);
+ RaiseLocalEvent(entity, ref ev);
+
+ if (ev.Autostand != entity.Comp.AutoStand)
+ SetAutoStand(entity!, ev.Autostand);
+
+ if (ev.Message != null)
+ {
+ _popup.PopupClient(ev.Message.Value.Item1, entity, entity, ev.Message.Value.Item2);
+ }
+
+ return !ev.Cancelled;
+ }
+
+ private bool StandingBlocked(Entity<KnockedDownComponent> entity)
+ {
+ if (!TryStand(entity))
+ return true;
+
+ if (!IntersectingStandingColliders(entity.Owner))
+ return false;
+
+ _popup.PopupClient(Loc.GetString("knockdown-component-stand-no-room"), entity, entity, PopupType.SmallCaution);
+ SetAutoStand(entity.Owner);
+ return true;
+
+ }
+
+ private void OnForceStandup(ForceStandUpEvent msg, EntitySessionEventArgs args)
+ {
+ if (args.SenderSession.AttachedEntity is not {} user)
+ return;
+
+ ForceStandUp(user);
+ }
+
+ public void ForceStandUp(Entity<KnockedDownComponent?> entity)
+ {
+ if (!Resolve(entity, ref entity.Comp, false))
+ return;
+
+ // That way if we fail to stand, the game will try to stand for us when we are able to
+ SetAutoStand(entity, true);
+
+ if (!HasComp<StandingStateComponent>(entity) || StandingBlocked((entity, entity.Comp)))
+ return;
+
+ if (!_hands.TryGetEmptyHand(entity.Owner, out _))
+ return;
+
+ if (!TryForceStand(entity.Owner))
+ return;
+
+ // If we have a DoAfter, cancel it
+ CancelKnockdownDoAfter(entity);
+ // Remove Component
+ RemComp<KnockedDownComponent>(entity);
+
+ _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(entity):user} has force stood up from knockdown.");
+ }
+
+ private void OnKnockedDownAlert(Entity<KnockedDownComponent> entity, ref KnockedDownAlertEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ // If we're already trying to stand, or we fail to stand try forcing it
+ if (!TryStanding(entity.Owner, out entity.Comp.DoAfterId))
+ ForceStandUp(entity!);
+
+ DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.DoAfterId));
+ args.Handled = true;
+ }
+
+ private bool TryForceStand(Entity<StaminaComponent?> entity)
+ {
+ // Can't force stand if no Stamina.
+ if (!Resolve(entity, ref entity.Comp, false))
+ return false;
+
+ var ev = new TryForceStandEvent(entity.Comp.ForceStandStamina);
+ RaiseLocalEvent(entity, ref ev);
+
+ if (!Stamina.TryTakeStamina(entity, ev.Stamina, entity.Comp, visual: true))
+ {
+ _popup.PopupClient(Loc.GetString("knockdown-component-pushup-failure"), entity, entity, PopupType.MediumCaution);
+ return false;
+ }
+
+ _popup.PopupClient(Loc.GetString("knockdown-component-pushup-success"), entity, entity);
+ _audio.PlayPredicted(entity.Comp.ForceStandSuccessSound, entity.Owner, entity.Owner, AudioParams.Default.WithVariation(0.025f).WithVolume(5f));
+
+ return true;
+ }
+
+ /// <summary>
+ /// Checks if standing would cause us to collide with something and potentially get stuck.
+ /// Returns true if we will collide with something, and false if we will not.
+ /// </summary>
+ private bool IntersectingStandingColliders(Entity<TransformComponent?> entity)
+ {
+ if (!Resolve(entity, ref entity.Comp))
+ return false;
+
+ var intersecting = _physics.GetEntitiesIntersectingBody(entity, StandingStateSystem.StandingCollisionLayer, false);
+
+ if (intersecting.Count == 0)
+ return false;
+
+ var fixtureQuery = GetEntityQuery<FixturesComponent>();
+ var xformQuery = GetEntityQuery<TransformComponent>();
+
+ var ourAABB = _entityLookup.GetAABBNoContainer(entity, entity.Comp.LocalPosition, entity.Comp.LocalRotation);
+
+ foreach (var ent in intersecting)
+ {
+ if (!fixtureQuery.TryGetComponent(ent, out var fixtures))
+ continue;
+
+ if (!xformQuery.TryComp(ent, out var xformComp))
+ continue;
+
+ var xform = new Transform(xformComp.LocalPosition, xformComp.LocalRotation);
+
+ foreach (var fixture in fixtures.Fixtures.Values)
+ {
+ if (!fixture.Hard || (fixture.CollisionMask & StandingStateSystem.StandingCollisionLayer) != StandingStateSystem.StandingCollisionLayer)
+ continue;
+
+ for (var i = 0; i < fixture.Shape.ChildCount; i++)
+ {
+ var intersection = fixture.Shape.ComputeAABB(xform, i).IntersectPercentage(ourAABB);
+ if (intersection > 0.1f)
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ #endregion
+
+ #region Knockdown Extenders
+
+ private void OnDamaged(Entity<KnockedDownComponent> entity, ref DamageChangedEvent args)
+ {
+ // We only want to extend our knockdown timer if it would've prevented us from standing up
+ if (!args.InterruptsDoAfters || !args.DamageIncreased || args.DamageDelta == null || GameTiming.ApplyingState)
+ return;
+
+ if (args.DamageDelta.GetTotal() >= KnockdownDamageThreshold) // TODO: Unhardcode this
+ SetKnockdownTime(entity, GameTiming.CurTime + DefaultKnockedDuration);
+ }
+
+ #endregion
+
+ #region Action Blockers
+
+ private void OnStandUpAttempt(Entity<KnockedDownComponent> entity, ref StandAttemptEvent args)
+ {
+ if (entity.Comp.LifeStage <= ComponentLifeStage.Running)
+ args.Cancel();
+ }
+
+ private void OnBuckleAttempt(Entity<KnockedDownComponent> entity, ref BuckleAttemptEvent args)
+ {
+ if (args.User == entity && entity.Comp.NextUpdate > GameTiming.CurTime)
+ args.Cancelled = true;
+ }
+
+ #endregion
+
+ #region DoAfter
+
+ private void OnStandDoAfter(Entity<KnockedDownComponent> entity, ref TryStandDoAfterEvent args)
+ {
+ entity.Comp.DoAfterId = null;
+
+ if (args.Cancelled || StandingBlocked(entity))
+ {
+ DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.DoAfterId));
+ return;
+ }
+
+ RemComp<KnockedDownComponent>(entity);
+
+ _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(entity):user} has stood up from knockdown.");
+ }
+
+ #endregion
+
+ #region Movement and Friction
+
+ private void RefreshKnockedMovement(Entity<KnockedDownComponent> ent)
+ {
+ var ev = new KnockedDownRefreshEvent();
+ RaiseLocalEvent(ent, ref ev);
+
+ ent.Comp.SpeedModifier = ev.SpeedModifier;
+ ent.Comp.FrictionModifier = ev.FrictionModifier;
+
+ _movementSpeedModifier.RefreshMovementSpeedModifiers(ent);
+ _movementSpeedModifier.RefreshFrictionModifiers(ent);
+ }
+
+ private void OnRefreshKnockedSpeed(Entity<KnockedDownComponent> entity, ref RefreshMovementSpeedModifiersEvent args)
+ {
+ args.ModifySpeed(entity.Comp.SpeedModifier);
+ }
+
+ private void OnKnockedTileFriction(Entity<KnockedDownComponent> entity, ref TileFrictionEvent args)
+ {
+ args.Modifier *= entity.Comp.FrictionModifier;
+ }
+
+ private void OnRefreshFriction(Entity<KnockedDownComponent> entity, ref RefreshFrictionModifiersEvent args)
+ {
+ args.ModifyFriction(entity.Comp.FrictionModifier);
+ args.ModifyAcceleration(entity.Comp.FrictionModifier);
+ }
+
+ private void OnHandEquipped(Entity<KnockedDownComponent> entity, ref DidEquipHandEvent args)
+ {
+ RefreshKnockedMovement(entity);
+ }
+
+ private void OnHandUnequipped(Entity<KnockedDownComponent> entity, ref DidUnequipHandEvent args)
+ {
+ RefreshKnockedMovement(entity);
+ }
+
+ #endregion
+}
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
-using Content.Shared.Interaction;
+using Content.Shared.Alert;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory.Events;
using Content.Shared.Item;
-using Content.Shared.Bed.Sleep;
using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
using Content.Shared.Database;
+using Content.Shared.DoAfter;
using Content.Shared.Hands;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Throwing;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
-using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
-using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing;
namespace Content.Shared.Stunnable;
public abstract partial class SharedStunSystem : EntitySystem
{
[Dependency] protected readonly ActionBlockerSystem Blocker = default!;
+ [Dependency] protected readonly AlertsSystem Alerts = default!;
+ [Dependency] protected readonly IGameTiming GameTiming = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
- [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
- [Dependency] private readonly StandingStateSystem _standingState = default!;
+ [Dependency] protected readonly SharedDoAfterSystem DoAfter = default!;
+ [Dependency] protected readonly SharedStaminaSystem Stamina = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffect = default!;
- /// <summary>
- /// Friction modifier for knocked down players.
- /// Doesn't make them faster but makes them slow down... slower.
- /// </summary>
- public const float KnockDownModifier = 0.2f;
-
public override void Initialize()
{
- SubscribeLocalEvent<KnockedDownComponent, ComponentInit>(OnKnockInit);
- SubscribeLocalEvent<KnockedDownComponent, ComponentShutdown>(OnKnockShutdown);
- SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandAttempt);
-
SubscribeLocalEvent<SlowedDownComponent, ComponentInit>(OnSlowInit);
SubscribeLocalEvent<SlowedDownComponent, ComponentShutdown>(OnSlowRemove);
+ SubscribeLocalEvent<SlowedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
SubscribeLocalEvent<StunnedComponent, ComponentStartup>(UpdateCanMove);
SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(OnStunShutdown);
SubscribeLocalEvent<StunOnContactComponent, StartCollideEvent>(OnStunOnContactCollide);
- // helping people up if they're knocked down
- SubscribeLocalEvent<KnockedDownComponent, InteractHandEvent>(OnInteractHand);
- SubscribeLocalEvent<SlowedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
-
- SubscribeLocalEvent<KnockedDownComponent, TileFrictionEvent>(OnKnockedTileFriction);
-
// Attempt event subscriptions.
SubscribeLocalEvent<StunnedComponent, ChangeDirectionAttemptEvent>(OnAttempt);
SubscribeLocalEvent<StunnedComponent, UpdateCanMoveEvent>(OnMoveAttempt);
SubscribeLocalEvent<StunnedComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
SubscribeLocalEvent<MobStateComponent, MobStateChangedEvent>(OnMobStateChanged);
- // Stun Appearance Data
+ InitializeKnockdown();
InitializeAppearance();
}
return;
TryStun(args.OtherEntity, ent.Comp.Duration, true, status);
- TryKnockdown(args.OtherEntity, ent.Comp.Duration, true, status);
- }
-
- private void OnKnockInit(EntityUid uid, KnockedDownComponent component, ComponentInit args)
- {
- _standingState.Down(uid);
- }
-
- private void OnKnockShutdown(EntityUid uid, KnockedDownComponent component, ComponentShutdown args)
- {
- _standingState.Stand(uid);
- }
-
- private void OnStandAttempt(EntityUid uid, KnockedDownComponent component, StandAttemptEvent args)
- {
- if (component.LifeStage <= ComponentLifeStage.Running)
- args.Cancel();
+ TryKnockdown(args.OtherEntity, ent.Comp.Duration, ent.Comp.Refresh, ent.Comp.AutoStand);
}
private void OnSlowInit(EntityUid uid, SlowedDownComponent component, ComponentInit args)
_movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
}
- private void OnRefreshMovespeed(EntityUid uid, SlowedDownComponent component, RefreshMovementSpeedModifiersEvent args)
- {
- args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
- }
-
// TODO STUN: Make events for different things. (Getting modifiers, attempt events, informative events...)
/// <summary>
/// Stuns the entity, disallowing it from doing many interactions temporarily.
/// </summary>
- public bool TryStun(EntityUid uid, TimeSpan time, bool refresh,
- StatusEffectsComponent? status = null)
+ public bool TryStun(EntityUid uid, TimeSpan time, bool refresh, StatusEffectsComponent? status = null)
{
if (time <= TimeSpan.Zero)
return false;
/// <summary>
/// Knocks down the entity, making it fall to the ground.
/// </summary>
- public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh,
- StatusEffectsComponent? status = null)
+ public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh, bool autoStand = true, bool drop = true)
{
if (time <= TimeSpan.Zero)
return false;
- if (!Resolve(uid, ref status, false))
+ // Can't fall down if you can't actually be downed.
+ if (!HasComp<StandingStateComponent>(uid))
return false;
- if (!_statusEffect.TryAddStatusEffect<KnockedDownComponent>(uid, "KnockedDown", time, refresh))
+ var evAttempt = new KnockDownAttemptEvent(autoStand, drop);
+ RaiseLocalEvent(uid, ref evAttempt);
+
+ if (evAttempt.Cancelled)
return false;
- var ev = new KnockedDownEvent();
- RaiseLocalEvent(uid, ref ev);
+ // Initialize our component with the relevant data we need if we don't have it
+ if (EnsureComp<KnockedDownComponent>(uid, out var component))
+ {
+ RefreshKnockedMovement((uid, component));
+ CancelKnockdownDoAfter((uid, component));
+ }
+ else
+ {
+ // Only drop items the first time we want to fall...
+ if (drop)
+ {
+ var ev = new DropHandItemsEvent();
+ RaiseLocalEvent(uid, ref ev);
+ }
+
+ // Only update Autostand value if it's our first time being knocked down...
+ SetAutoStand((uid, component), evAttempt.AutoStand);
+ }
+
+ var knockedEv = new KnockedDownEvent(time);
+ RaiseLocalEvent(uid, ref knockedEv);
+
+ UpdateKnockdownTime((uid, component), knockedEv.Time, refresh);
+
+ Alerts.ShowAlert(uid, KnockdownAlert, null, (GameTiming.CurTime, component.NextUpdate));
+
+ _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} knocked down for {time.Seconds} seconds");
return true;
}
if (!Resolve(uid, ref status, false))
return false;
- return TryKnockdown(uid, time, refresh, status) && TryStun(uid, time, refresh, status);
+ return TryKnockdown(uid, time, refresh) && TryStun(uid, time, refresh, status);
}
/// <summary>
/// Slows down the mob's walking/running speed temporarily
/// </summary>
public bool TrySlowdown(EntityUid uid, TimeSpan time, bool refresh,
- float walkSpeedMultiplier = 1f, float runSpeedMultiplier = 1f,
+ float walkSpeedMod = 1f, float sprintSpeedMod = 1f,
StatusEffectsComponent? status = null)
{
if (!Resolve(uid, ref status, false))
{
var slowed = Comp<SlowedDownComponent>(uid);
// Doesn't make much sense to have the "TrySlowdown" method speed up entities now does it?
- walkSpeedMultiplier = Math.Clamp(walkSpeedMultiplier, 0f, 1f);
- runSpeedMultiplier = Math.Clamp(runSpeedMultiplier, 0f, 1f);
+ walkSpeedMod = Math.Clamp(walkSpeedMod, 0f, 1f);
+ sprintSpeedMod = Math.Clamp(sprintSpeedMod, 0f, 1f);
- slowed.WalkSpeedModifier *= walkSpeedMultiplier;
- slowed.SprintSpeedModifier *= runSpeedMultiplier;
+ slowed.WalkSpeedModifier *= walkSpeedMod;
+ slowed.SprintSpeedModifier *= sprintSpeedMod;
_movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
return true;
UpdateStunModifiers(ent, speedModifier, speedModifier);
}
- private void OnInteractHand(EntityUid uid, KnockedDownComponent knocked, InteractHandEvent args)
- {
- if (args.Handled || knocked.HelpTimer > 0f)
- return;
-
- // TODO: This should be an event.
- if (HasComp<SleepingComponent>(uid))
- return;
-
- // Set it to half the help interval so helping is actually useful...
- knocked.HelpTimer = knocked.HelpInterval / 2f;
-
- _statusEffect.TryRemoveTime(uid, "KnockedDown", TimeSpan.FromSeconds(knocked.HelpInterval));
- _audio.PlayPredicted(knocked.StunAttemptSound, uid, args.User);
- Dirty(uid, knocked);
+ #region friction and movement listeners
- args.Handled = true;
- }
-
- private void OnKnockedTileFriction(EntityUid uid, KnockedDownComponent component, ref TileFrictionEvent args)
+ private void OnRefreshMovespeed(EntityUid ent, SlowedDownComponent comp, RefreshMovementSpeedModifiersEvent args)
{
- args.Modifier *= KnockDownModifier;
+ args.ModifySpeed(comp.WalkSpeedModifier, comp.SprintSpeedModifier);
}
+ #endregion
+
#region Attempt Event Handling
private void OnMoveAttempt(EntityUid uid, StunnedComponent stunned, UpdateCanMoveEvent args)
#endregion
}
-
-/// <summary>
-/// Raised directed on an entity when it is stunned.
-/// </summary>
-[ByRefEvent]
-public record struct StunnedEvent;
-
-/// <summary>
-/// Raised directed on an entity when it is knocked down.
-/// </summary>
-[ByRefEvent]
-public record struct KnockedDownEvent;
[DataField]
public TimeSpan Duration = TimeSpan.FromSeconds(5);
+ /// <summary>
+ /// Should the stun applied refresh?
+ /// </summary>
+ [DataField]
+ public bool Refresh = true;
+
+ /// <summary>
+ /// Should the stunned entity try to stand up when knockdown ends?
+ /// </summary>
+ [DataField]
+ public bool AutoStand = true;
+
[DataField]
public EntityWhitelist Blacklist = new();
}
--- /dev/null
+using Content.Shared.Alert;
+using Content.Shared.DoAfter;
+using Content.Shared.Popups;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Stunnable;
+
+/// <summary>
+/// This contains all the events raised by the SharedStunSystem
+/// </summary>
+
+/// <summary>
+/// Raised directed on an entity when it is stunned.
+/// </summary>
+[ByRefEvent]
+public record struct StunnedEvent;
+
+/// <summary>
+/// Raised directed on an entity before it is knocked down to see if it should be cancelled, and to determine
+/// knocked down arguments.
+/// </summary>
+[ByRefEvent]
+public record struct KnockDownAttemptEvent(bool AutoStand, bool Drop)
+{
+ public bool Cancelled;
+}
+
+/// <summary>
+/// Raised directed on an entity when it is knocked down.
+/// </summary>
+[ByRefEvent]
+public record struct KnockedDownEvent(TimeSpan Time);
+
+/// <summary>
+/// Raised on an entity that needs to refresh its knockdown modifiers
+/// </summary>
+[ByRefEvent]
+public record struct KnockedDownRefreshEvent()
+{
+ public float SpeedModifier = 1f;
+ public float FrictionModifier = 1f;
+}
+
+/// <summary>
+/// Raised directed on an entity when it tries to stand up
+/// </summary>
+/// <param name="Autostand">If the attempt was cancelled, passes a recommended value to change autostand to.</param>
+[ByRefEvent]
+public record struct StandUpAttemptEvent(bool Autostand)
+{
+ public bool Cancelled = false;
+
+ // Popup data to display to the entity if we so desire...
+ public (string, PopupType)? Message = null;
+}
+
+/// <summary>
+/// Raises the default DoAfterTime for a stand-up attempt for other components to modify it.
+/// </summary>
+/// <param name="DoAfterTime"></param>
+[ByRefEvent]
+public record struct GetStandUpTimeEvent(TimeSpan DoAfterTime);
+
+/// <summary>
+/// Raised when an entity is forcing itself to stand, allows for the stamina damage it is taking to be modified.
+/// This is raised before the stamina damage is taken so it can still fail if the entity does not have enough stamina.
+/// </summary>
+/// <param name="Stamina">The stamina damage the entity will take when it forces itself to stand.</param>
+[ByRefEvent]
+public record struct TryForceStandEvent(float Stamina);
+
+/// <summary>
+/// Raised when you click on the Knocked Down Alert
+/// </summary>
+public sealed partial class KnockedDownAlertEvent : BaseAlertEvent;
+
+/// <summary>
+/// The DoAfterEvent for trying to stand the slow and boring way.
+/// </summary>
+[ByRefEvent]
+[Serializable, NetSerializable]
+public sealed partial class TryStandDoAfterEvent : SimpleDoAfterEvent;
+
+/// <summary>
+/// An event sent by the client to the server to ask it very nicely to perform a forced stand-up.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class ForceStandUpEvent : EntityEventArgs;
+
/// Raised when we try to pushback an entity from throwing
/// </summary>
public sealed class ThrowPushbackAttemptEvent : CancellableEntityEventArgs {}
+
+ /// <summary>
+ /// Raised on an entity that is being pushed from a thrown entity
+ /// </summary>
+ [ByRefEvent]
+ public record struct ThrowerImpulseEvent()
+ {
+ public bool Push;
+ };
}
_recoil.KickCamera(user.Value, -direction * 0.04f);
// Give thrower an impulse in the other direction
- if (pushbackRatio != 0.0f &&
- physics.Mass > 0f &&
- TryComp(user.Value, out PhysicsComponent? userPhysics) &&
- _gravity.IsWeightless(user.Value, userPhysics))
- {
- var msg = new ThrowPushbackAttemptEvent();
- RaiseLocalEvent(uid, msg);
- const float massLimit = 5f;
+ if (pushbackRatio == 0.0f ||
+ physics.Mass == 0f ||
+ !TryComp(user.Value, out PhysicsComponent? userPhysics))
+ return;
+ var msg = new ThrowPushbackAttemptEvent();
+ RaiseLocalEvent(uid, msg);
- if (!msg.Cancelled)
- _physics.ApplyLinearImpulse(user.Value, -impulseVector / physics.Mass * pushbackRatio * MathF.Min(massLimit, physics.Mass), body: userPhysics);
- }
+ if (msg.Cancelled)
+ return;
+
+ var pushEv = new ThrowerImpulseEvent();
+ RaiseLocalEvent(user.Value, ref pushEv);
+ const float massLimit = 5f;
+
+ if (pushEv.Push || _gravity.IsWeightless(user.Value))
+ _physics.ApplyLinearImpulse(user.Value, -impulseVector / physics.Mass * pushbackRatio * MathF.Min(massLimit, physics.Mass), body: userPhysics);
}
}
var shotEv = new GunShotEvent(user, ev.Ammo);
RaiseLocalEvent(gunUid, ref shotEv);
- if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics))
- {
- if (_gravity.IsWeightless(user, userPhysics))
- CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
- }
+ if (!userImpulse || !TryComp<PhysicsComponent>(user, out var userPhysics))
+ return;
+
+ var shooterEv = new ShooterImpulseEvent();
+ RaiseLocalEvent(user, ref shooterEv);
+
+ if (shooterEv.Push || _gravity.IsWeightless(user, userPhysics))
+ CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
}
public void Shoot(
[ByRefEvent]
public record struct GunShotEvent(EntityUid User, List<(EntityUid? Uid, IShootable Shootable)> Ammo);
+/// <summary>
+/// Raised on an entity after firing a gun to see if any components or systems would allow this entity to be pushed
+/// by the gun they're firing. If true, GunSystem will create an impulse on our entity.
+/// </summary>
+[ByRefEvent]
+public record struct ShooterImpulseEvent()
+{
+ public bool Push;
+};
+
public enum EffectLayers : byte
{
Unshaded,
admin-smite-nyanify-name = Cat Ears
admin-smite-kill-sign-name = Kill Sign
admin-smite-omni-accent-name = Omni-Accent
+admin-smite-crawler-name = Crawler
## Smite descriptions
admin-smite-terminate-description = Creates a Terminator ghost role with the sole objective of killing them.
admin-smite-super-slip-description = Slips them really, really hard.
admin-smite-omni-accent-description = Makes the target speak with almost every accent available.
+admin-smite-crawler-description = Makes the target fall down and be unable to stand up. Remove their hands too for added effect!
## Tricks descriptions
alerts-stunned-name = [color=yellow]Stunned[/color]
alerts-stunned-desc = You're [color=yellow]stunned[/color]! Something is impairing your ability to move or interact with objects.
+alerts-knockdown-name = [color=yellow]Knocked Down[/color]
+alerts-knockdown-desc = You're [color=yellow]Knocked Down[/color]! Something has slipped or pushed you over, encumbering your movement.
+
alerts-handcuffed-name = [color=yellow]Handcuffed[/color]
alerts-handcuffed-desc = You're [color=yellow]handcuffed[/color] and can't use your hands. If anyone drags you, you won't be able to resist.
ui-options-function-move-down = Move Down
ui-options-function-move-right = Move Right
ui-options-function-walk = Walk
+ui-options-function-toggle-knockdown = Toggle Crawling
ui-options-function-camera-rotate-left = Rotate left
ui-options-function-camera-rotate-right = Rotate right
stunnable-component-disarm-success-others = {CAPITALIZE(THE($source))} pushes {THE($target)}!
stunnable-component-disarm-success = You push {THE($target)}!
+knockdown-component-pushup-failure = You're too exhausted to push yourself up!
+knockdown-component-pushup-success = With a burst of energy you push yourself up!
+knockdown-component-stand-no-room = You try to push yourself to stand up but there's not enough room!
+worm-component-stand-attempt = You try to stand up but you cannot!
name: alerts-stunned-name
description: alerts-stunned-desc
+- type: alert
+ id: Knockdown
+ clickEvent: !type:KnockedDownAlertEvent
+ icons:
+ - sprite: /Textures/Interface/Alerts/stunnable.rsi
+ state: knocked-down
+ name: alerts-knockdown-name
+ description: alerts-knockdown-desc
+
- type: alert
id: Handcuffed
clickEvent: !type:RemoveCuffsAlertEvent
components:
- type: Clickable
- type: Slippery
+ staminaDamage: 0
+ frictionStatusTime: 0 # Don't apply friction twice
- type: Transform
noRot: true
anchored: true
- type: StatusEffects
allowed:
- Stun
- - KnockedDown
- SlowedDown
- Flashed
- type: TypingIndicator
- type: StatusEffects
allowed:
- Stun
- - KnockedDown
- SlowedDown
- Stutter
- Electrocution
- type: StatusEffects
allowed:
- Stun
- - KnockedDown
- SlowedDown
- Stutter
- Electrocution
- type: StatusEffects
allowed:
- Stun
- - KnockedDown
- SlowedDown
- Stutter
- Electrocution
- type: StatusEffects
allowed:
- Stun
- - KnockedDown
+ - Friction
- SlowedDown
- Stutter
- Electrocution
- type: TrashOnSolutionEmpty
solution: drink
+- type: entity
+ parent: BluespaceBeaker
+ id: BottomlessLube
+ name: bottomless lube beaker
+ suffix: DEBUG
+ description: This anomalous beaker infinitely produces space lube and as such is to be closely guarded such that it doesn't fall in the wrong hands.
+ components:
+ - type: SolutionContainerManager
+ solutions:
+ beaker:
+ maxVol: 1000
+ reagents:
+ - ReagentId: SpaceLube
+ Quantity: 1000
+ - type: SolutionRegeneration
+ solution: beaker
+ generated:
+ reagents:
+ - ReagentId: SpaceLube
+ Quantity: 200
# Mopwata
- type: weightedRandomFillSolution
Quantity: 5
- type: Slippery
slipData:
- paralyzeTime: 3
+ knockdownTime: 3
launchForwardsMultiplier: 3
- type: StepTrigger
intersectRatio: 0.2
- type: UseDelay
delay: 0.8
- type: Slippery
+ staminaDamage: 0
slipData:
- paralyzeTime: 0
+ stunTime: 0
+ knockdownTime: 0
launchForwardsMultiplier: 0
slipSound:
collection: Parp
- type: SolutionContainerVisuals
fillBaseName: syndie-
- type: Slippery
+ staminaDamage: 50
slipData:
- paralyzeTime: 3
+ knockdownTime: 3
launchForwardsMultiplier: 3
- type: Item
heldPrefix: syndie
layers:
- state: syndie-soaplet
- type: Slippery
- slipData:
- paralyzeTime: 1.5 # these things are tiny
- launchForwardsMultiplier: 1.5
- type: StepTrigger
intersectRatio: 0.04
- type: Item
- type: SolutionContainerVisuals
fillBaseName: omega-
- type: Slippery
+ staminaDamage: 50
slipData:
- paralyzeTime: 5.0
+ knockdownTime: 5.0
launchForwardsMultiplier: 3.0
- type: Item
heldPrefix: omega
name: taser
parent: [BaseWeaponBatterySmall, BaseSecurityContraband]
id: WeaponTaser
- description: A low-capacity, energy-based stun gun used by security teams to subdue targets at range.
+ description: A low-capacity, energy-based stun gun used by security teams to subdue targets at close range.
components:
- type: Tag
tags:
zeroVisible: true
- type: Appearance
+- type: entity
+ name: elite taser
+ parent: [ BaseCentcommContraband, WeaponTaser ]
+ id: WeaponTaserSuper
+ suffix: ADMEME
+ description: A low-capacity, energy-based stun gun used by elite security teams to disable even the toughest of targets.
+ components:
+ - type: Gun
+ fireRate: 0.5
+ soundGunshot:
+ path: /Audio/Effects/tesla_collapse.ogg # The wrath of god...
+ params:
+ volume: -6
+ - type: ProjectileBatteryAmmoProvider
+ proto: BulletTaserSuper
+ fireCost: 200
+
- type: entity
name: antique laser pistol
parent: [BaseWeaponBatterySmall, BaseGrandTheftContraband]
- type: Projectile
damage:
types:
- Heat: 5
+ Shock: 1
soundHit:
path: "/Audio/Weapons/Guns/Hits/taser_hit.ogg"
forceSound: true
+ - type: TimedDespawn
+ lifetime: 0.170 # Very short range
+ - type: StunOnCollide
+ stunAmount: 0
+ knockdownAmount: 2.5 # Enough to subdue and follow up with a stun batong
+ slowdownAmount: 2.5
+ walkSpeedModifier: 0.5
+ sprintSpeedModifier: 0.5
+
+- type: entity
+ parent: BulletTaser
+ id: BulletTaserSuper
+ categories: [ HideSpawnMenu ]
+ name: taser bolt
+ description: If you can see this, you've probably been stun-meta'd
+ components:
+ - type: Sprite
+ noRot: true
+ sprite: Structures/Power/Generation/Tesla/energy_miniball.rsi
+ color: "#ffff33"
+ layers:
+ - state: tesla_projectile
+ shader: unshaded
+ - type: PointLight
+ enabled: true
+ color: "#ffff33"
+ radius: 2.0
+ energy: 7.01
+ - type: TimedDespawn
+ lifetime: 1.0 # Not so short range
- type: StunOnCollide
stunAmount: 5
- knockdownAmount: 5
+ knockdownAmount: 10
+ slowdownAmount: 10
+ walkSpeedModifier: 0.5
+ sprintSpeedModifier: 0.5
- type: entity
name : disabler bolt
components:
- type: DrowsinessStatusEffect
+# Makes you more slippery, or perhaps less slippery.
+- type: entity
+ parent: MobStatusEffectBase
+ id: StatusEffectFriction
+ name: friction
+ components:
+ - type: FrictionStatusEffect
+
# Adds drugs overlay
- type: entity
parent: MobStatusEffectBase
id: StatusEffectSeeingRainbow
name: hallucinations
components:
- - type: SeeingRainbowsStatusEffect
\ No newline at end of file
+ - type: SeeingRainbowsStatusEffect
meltingPoint: 18.2
tileReactions:
- !type:SpillTileReaction
- friction: 0.0
+ friction: 0.05
- type: reagent
id: SpaceGlue
id: Stun
alert: Stun
-- type: statusEffect
- id: KnockedDown
- alert: Stun
-
- type: statusEffect
id: SlowedDown
--- /dev/null
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Made by Princess Chesseballs, Pronana on Github https://github.com/Pronana",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "knocked-down"
+ }
+ ]
+}
mod1: Alt
- function: OpenCharacterMenu
type: State
- key: C
+ key: U
- function: OpenEmotesMenu
type: State
key: Y
- function: Arcade3
type: State
key: Z
+- function: ToggleKnockdown
+ type: State
+ key: C
- function: OpenAbilitiesMenu
type: State
key: K