Crawling Part 1: The Knockdownening (#36881)

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
This commit is contained in:
Princess Cheeseballs
2025-07-19 16:54:42 -07:00
committed by GitHub
parent cfb0a95035
commit dec2d42a1d
60 changed files with 1595 additions and 220 deletions

View File

@@ -55,6 +55,7 @@ namespace Content.Client.Input
human.AddFunction(EngineKeyFunctions.MoveLeft); human.AddFunction(EngineKeyFunctions.MoveLeft);
human.AddFunction(EngineKeyFunctions.MoveRight); human.AddFunction(EngineKeyFunctions.MoveRight);
human.AddFunction(EngineKeyFunctions.Walk); human.AddFunction(EngineKeyFunctions.Walk);
human.AddFunction(ContentKeyFunctions.ToggleKnockdown);
human.AddFunction(ContentKeyFunctions.SwapHands); human.AddFunction(ContentKeyFunctions.SwapHands);
human.AddFunction(ContentKeyFunctions.SwapHandsReverse); human.AddFunction(ContentKeyFunctions.SwapHandsReverse);
human.AddFunction(ContentKeyFunctions.Drop); human.AddFunction(ContentKeyFunctions.Drop);

View File

@@ -162,6 +162,7 @@ namespace Content.Client.Options.UI.Tabs
AddButton(EngineKeyFunctions.Walk); AddButton(EngineKeyFunctions.Walk);
AddCheckBox("ui-options-hotkey-toggle-walk", _cfg.GetCVar(CCVars.ToggleWalk), HandleToggleWalk); AddCheckBox("ui-options-hotkey-toggle-walk", _cfg.GetCVar(CCVars.ToggleWalk), HandleToggleWalk);
InitToggleWalk(); InitToggleWalk();
AddButton(ContentKeyFunctions.ToggleKnockdown);
AddHeader("ui-options-header-camera"); AddHeader("ui-options-header-camera");
AddButton(EngineKeyFunctions.CameraRotateLeft); AddButton(EngineKeyFunctions.CameraRotateLeft);

View File

@@ -1,17 +1,19 @@
using System.Numerics; using System.Numerics;
using Content.Shared.Mobs; using Content.Shared.CombatMode;
using Content.Shared.Interaction;
using Content.Shared.Stunnable; using Content.Shared.Stunnable;
using Robust.Client.Animations; using Robust.Client.Animations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.Animations; using Robust.Shared.Animations;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Stunnable; namespace Content.Client.Stunnable;
public sealed class StunSystem : SharedStunSystem public sealed class StunSystem : SharedStunSystem
{ {
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SpriteSystem _spriteSystem = default!; [Dependency] private readonly SpriteSystem _spriteSystem = default!;
@@ -23,6 +25,22 @@ public sealed class StunSystem : SharedStunSystem
SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit); SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<StunVisualsComponent, AppearanceChangeEvent>(OnAppearanceChanged); 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> /// <summary>

View File

@@ -39,6 +39,7 @@ using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Slippery; using Content.Shared.Slippery;
using Content.Shared.Stunnable;
using Content.Shared.Tabletop.Components; using Content.Shared.Tabletop.Components;
using Content.Shared.Tools.Systems; using Content.Shared.Tools.Systems;
using Content.Shared.Verbs; using Content.Shared.Verbs;
@@ -48,6 +49,7 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Systems;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer; using Timer = Robust.Shared.Timing.Timer;
@@ -877,7 +879,7 @@ public sealed partial class AdminVerbSystem
if (!hadSlipComponent) if (!hadSlipComponent)
{ {
slipComponent.SlipData.SuperSlippery = true; slipComponent.SlipData.SuperSlippery = true;
slipComponent.SlipData.ParalyzeTime = TimeSpan.FromSeconds(5); slipComponent.SlipData.StunTime = TimeSpan.FromSeconds(5);
slipComponent.SlipData.LaunchForwardsMultiplier = 20; slipComponent.SlipData.LaunchForwardsMultiplier = 20;
} }
@@ -922,5 +924,20 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description")) Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
}; };
args.Verbs.Add(omniaccent); 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);
} }
} }

View File

@@ -338,6 +338,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Ensure we actually have the component // Ensure we actually have the component
EnsureComp<TileFrictionModifierComponent>(entity); 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 // 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. // the sprite threshold for a puddle larger than 5 pixels.
var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold); var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
@@ -356,17 +358,21 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
var launchMult = FixedPoint2.Zero; var launchMult = FixedPoint2.Zero;
// A cumulative weighted amount of stun times from slippery reagents // A cumulative weighted amount of stun times from slippery reagents
var stunTimer = TimeSpan.Zero; 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 // Check if the puddle is big enough to slip in to avoid doing unnecessary logic
if (solution.Volume <= smallPuddleThreshold) if (solution.Volume <= smallPuddleThreshold)
{ {
_stepTrigger.SetActive(entity, false, comp); _stepTrigger.SetActive(entity, false, comp);
_tile.SetModifier(entity, 1f); _tile.SetModifier(entity, 1f);
slipComp.SlipData.SlipFriction = 1f;
slipComp.AffectsSliding = false;
Dirty(entity, slipComp);
return; return;
} }
if (!TryComp<SlipperyComponent>(entity, out var slipComp)) slipComp.AffectsSliding = true;
return;
foreach (var (reagent, quantity) in solution.Contents) foreach (var (reagent, quantity) in solution.Contents)
{ {
@@ -386,7 +392,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Aggregate launch speed based on quantity // Aggregate launch speed based on quantity
launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity; launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity;
// Aggregate stun times based on 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) if (reagentProto.SlipData.SuperSlippery)
superSlipperyUnits += quantity; superSlipperyUnits += quantity;
@@ -404,8 +411,9 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// 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. // 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) if (slipperyUnits > 0)
{ {
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits); slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
slipComp.SlipData.ParalyzeTime = stunTimer / (float)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 // Only make it super slippery if there is enough super slippery units for its own puddle

View File

@@ -8,21 +8,47 @@ namespace Content.Server.Stunnable.Components
{ {
// TODO: Can probably predict this. // TODO: Can probably predict this.
// See stunsystem for what these do /// <summary>
[DataField("stunAmount")] /// How long we are stunned for
public int StunAmount; /// </summary>
[DataField]
public TimeSpan StunAmount;
[DataField("knockdownAmount")] /// <summary>
public int KnockdownAmount; /// How long we are knocked down for
/// </summary>
[DataField]
public TimeSpan KnockdownAmount;
[DataField("slowdownAmount")] /// <summary>
public int SlowdownAmount; /// How long we are slowed down for
/// </summary>
[DataField]
public TimeSpan SlowdownAmount;
[DataField("walkSpeedMultiplier")] /// <summary>
public float WalkSpeedMultiplier = 1f; /// Multiplier for a mob's walking speed
/// </summary>
[DataField]
public float WalkSpeedModifier = 1f;
[DataField("runSpeedMultiplier")] /// <summary>
public float RunSpeedMultiplier = 1f; /// Multiplier for a mob's sprinting speed
/// </summary>
[DataField]
public float SprintSpeedModifier = 1f;
/// <summary>
/// Refresh Stun or Slowdown on hit
/// </summary>
[DataField]
public bool Refresh = true;
/// <summary>
/// Should the entity try and stand automatically after being knocked down?
/// </summary>
[DataField]
public bool AutoStand = true;
/// <summary> /// <summary>
/// Fixture we track for the collision. /// Fixture we track for the collision.

View File

@@ -0,0 +1,6 @@
using Content.Shared.Stunnable;
namespace Content.Server.Stunnable;
public sealed class StunSystem : SharedStunSystem;

View File

@@ -22,17 +22,14 @@ namespace Content.Server.Stunnable
private void TryDoCollideStun(EntityUid uid, StunOnCollideComponent component, EntityUid target) 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, component.StunAmount, component.Refresh, status);
{
_stunSystem.TryStun(target, TimeSpan.FromSeconds(component.StunAmount), true, status);
_stunSystem.TryKnockdown(target, TimeSpan.FromSeconds(component.KnockdownAmount), true, _stunSystem.TryKnockdown(target, component.KnockdownAmount, component.Refresh, component.AutoStand);
status);
_stunSystem.TrySlowdown(target, TimeSpan.FromSeconds(component.SlowdownAmount), true, _stunSystem.TrySlowdown(target, component.SlowdownAmount, component.Refresh, component.WalkSpeedModifier, component.SprintSpeedModifier, status);
component.WalkSpeedMultiplier, component.RunSpeedMultiplier, status);
}
} }
private void HandleCollide(EntityUid uid, StunOnCollideComponent component, ref StartCollideEvent args) private void HandleCollide(EntityUid uid, StunOnCollideComponent component, ref StartCollideEvent args)
{ {

View File

@@ -1,7 +0,0 @@
using Content.Shared.Stunnable;
namespace Content.Server.Stunnable
{
public sealed class StunSystem : SharedStunSystem
{}
}

View File

@@ -108,7 +108,6 @@ public sealed partial class SleepingSystem : EntitySystem
{ {
// Expiring status effects would remove the components needed for sleeping // Expiring status effects would remove the components needed for sleeping
_statusEffectOld.TryRemoveStatusEffect(ent.Owner, "Stun"); _statusEffectOld.TryRemoveStatusEffect(ent.Owner, "Stun");
_statusEffectOld.TryRemoveStatusEffect(ent.Owner, "KnockedDown");
EnsureComp<StunnedComponent>(ent); EnsureComp<StunnedComponent>(ent);
EnsureComp<KnockedDownComponent>(ent); EnsureComp<KnockedDownComponent>(ent);

View File

@@ -33,6 +33,24 @@ public sealed partial class HandcuffComponent : Component
[DataField, ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
public float StunBonus = 2f; 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> /// <summary>
/// Will the cuffs break when removed? /// Will the cuffs break when removed?
/// </summary> /// </summary>

View File

@@ -21,6 +21,7 @@ using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Item; using Content.Shared.Item;
using Content.Shared.Movement.Events; using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Events; using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Pulling.Events; using Content.Shared.Pulling.Events;
using Content.Shared.Rejuvenate; using Content.Shared.Rejuvenate;
@@ -45,6 +46,7 @@ namespace Content.Shared.Cuffs
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!; [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly MovementSpeedModifierSystem _move = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
@@ -56,10 +58,14 @@ namespace Content.Shared.Cuffs
[Dependency] private readonly UseDelaySystem _delay = default!; [Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!; [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
private EntityQuery<HandcuffComponent> _cuffQuery;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
_cuffQuery = GetEntityQuery<HandcuffComponent>();
SubscribeLocalEvent<CuffableComponent, HandCountChangedEvent>(OnHandCountChanged); SubscribeLocalEvent<CuffableComponent, HandCountChangedEvent>(OnHandCountChanged);
SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt); SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
@@ -89,6 +95,9 @@ namespace Content.Shared.Cuffs
SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit); SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
SubscribeLocalEvent<HandcuffComponent, AddCuffDoAfterEvent>(OnAddCuffDoAfter); SubscribeLocalEvent<HandcuffComponent, AddCuffDoAfterEvent>(OnAddCuffDoAfter);
SubscribeLocalEvent<HandcuffComponent, VirtualItemDeletedEvent>(OnCuffVirtualItemDeleted); 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) private void CheckInteract(Entity<CuffableComponent> ent, ref InteractionAttemptEvent args)
@@ -366,6 +375,9 @@ namespace Content.Shared.Cuffs
_adminLog.Add(LogType.Action, LogImpact.High, _adminLog.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(user):player} has cuffed {ToPrettyString(target):player}"); $"{ToPrettyString(user):player} has cuffed {ToPrettyString(target):player}");
} }
if (!MathHelper.CloseTo(component.MovementMod, 1f))
_move.RefreshMovementSpeedModifiers(target);
} }
else else
{ {
@@ -420,6 +432,72 @@ namespace Content.Shared.Cuffs
} }
} }
/// <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> /// <summary>
/// Adds virtual cuff items to the user's hands. /// Adds virtual cuff items to the user's hands.
/// </summary> /// </summary>
@@ -736,6 +814,9 @@ namespace Content.Shared.Cuffs
shoved = true; shoved = true;
} }
if (!MathHelper.CloseTo(cuff.MovementMod, 1f))
_move.RefreshMovementSpeedModifiers(target);
if (cuffable.CuffedHandCount == 0) if (cuffable.CuffedHandCount == 0)
{ {
if (user != null) if (user != null)

View File

@@ -1,6 +1,7 @@
using System.Numerics; using System.Numerics;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
@@ -38,7 +39,7 @@ public sealed partial class StaminaComponent : Component
public float StaminaDamage; public float StaminaDamage;
/// <summary> /// <summary>
/// How much stamina damage is required to entire stam crit. /// How much stamina damage is required to enter stam crit.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField] [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
public float CritThreshold = 100f; public float CritThreshold = 100f;
@@ -71,6 +72,18 @@ public sealed partial class StaminaComponent : Component
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public float AfterCritDecayMultiplier = 5f; 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> /// <summary>
/// Thresholds that determine an entity's slowdown as a function of stamina damage. /// Thresholds that determine an entity's slowdown as a function of stamina damage.
/// </summary> /// </summary>

View File

@@ -234,7 +234,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem
/// <summary> /// <summary>
/// Tries to take stamina damage without raising the entity over the crit threshold. /// Tries to take stamina damage without raising the entity over the crit threshold.
/// </summary> /// </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 // Something that has no Stamina component automatically passes stamina checks
if (!Resolve(uid, ref component, false)) if (!Resolve(uid, ref component, false))
@@ -242,10 +242,10 @@ public abstract partial class SharedStaminaSystem : EntitySystem
var oldStam = component.StaminaDamage; var oldStam = component.StaminaDamage;
if (oldStam + value > component.CritThreshold || component.Critical) if (oldStam + value >= component.CritThreshold || component.Critical)
return false; return false;
TakeStaminaDamage(uid, value, component, source, with, visual: false); TakeStaminaDamage(uid, value, component, source, with, visual: visual);
return true; return true;
} }

View File

@@ -0,0 +1,31 @@
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);
}
}

View File

@@ -36,6 +36,7 @@ public abstract partial class SharedHandsSystem
InitializeDrop(); InitializeDrop();
InitializePickup(); InitializePickup();
InitializeRelay(); InitializeRelay();
InitializeEventListeners();
SubscribeLocalEvent<HandsComponent, ComponentInit>(OnInit); SubscribeLocalEvent<HandsComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<HandsComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<HandsComponent, MapInitEvent>(OnMapInit);
@@ -166,6 +167,26 @@ public abstract partial class SharedHandsSystem
return false; 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> /// <summary>
/// Attempts to retrieve the item held in the entity's active hand. /// Attempts to retrieve the item held in the entity's active hand.
/// </summary> /// </summary>

View File

@@ -5,6 +5,7 @@ namespace Content.Shared.Input
[KeyFunctions] [KeyFunctions]
public static class ContentKeyFunctions public static class ContentKeyFunctions
{ {
public static readonly BoundKeyFunction ToggleKnockdown = "ToggleKnockdown";
public static readonly BoundKeyFunction UseItemInHand = "ActivateItemInHand"; public static readonly BoundKeyFunction UseItemInHand = "ActivateItemInHand";
public static readonly BoundKeyFunction AltUseItemInHand = "AltActivateItemInHand"; public static readonly BoundKeyFunction AltUseItemInHand = "AltActivateItemInHand";
public static readonly BoundKeyFunction ActivateItemInWorld = "ActivateItemInWorld"; public static readonly BoundKeyFunction ActivateItemInWorld = "ActivateItemInWorld";

View File

@@ -0,0 +1,23 @@
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;
}

View File

@@ -0,0 +1,23 @@
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;
}

View File

@@ -0,0 +1,100 @@
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);
}
}

View File

@@ -1,8 +1,7 @@
using System.Text.Json.Serialization.Metadata;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events; using Content.Shared.Standing;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -11,7 +10,7 @@ namespace Content.Shared.Movement.Systems
public sealed class MovementSpeedModifierSystem : EntitySystem public sealed class MovementSpeedModifierSystem : EntitySystem
{ {
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IConfigurationManager _configManager = default!;
private float _frictionModifier; private float _frictionModifier;
private float _airDamping; private float _airDamping;
@@ -21,6 +20,8 @@ namespace Content.Shared.Movement.Systems
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<MovementSpeedModifierComponent, MapInitEvent>(OnModMapInit); 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.TileFrictionModifier, value => _frictionModifier = value, true);
Subs.CVar(_configManager, CCVars.AirFriction, value => _airDamping = value, true); Subs.CVar(_configManager, CCVars.AirFriction, value => _airDamping = value, true);
@@ -41,6 +42,18 @@ namespace Content.Shared.Movement.Systems
Dirty(ent); 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) public void RefreshWeightlessModifiers(EntityUid uid, MovementSpeedModifierComponent? move = null)
{ {
if (!Resolve(uid, ref move, false)) if (!Resolve(uid, ref move, false))

View File

@@ -0,0 +1,53 @@
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;
}
}

View File

@@ -8,15 +8,15 @@ namespace Content.Shared.Slippery;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class SlidingComponent : Component public sealed partial class SlidingComponent : Component
{ {
/// <summary>
/// A list of SuperSlippery entities the entity with this component is colliding with.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<EntityUid> CollidingEntities = new ();
/// <summary> /// <summary>
/// The friction modifier that will be applied to any friction calculations. /// The friction modifier that will be applied to any friction calculations.
/// </summary> /// </summary>
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public float FrictionModifier; public float FrictionModifier;
/// <summary>
/// Hashset of contacting entities.
/// </summary>
[DataField]
public HashSet<EntityUid> Contacting = new();
} }

View File

@@ -1,19 +1,53 @@
using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems;
using Content.Shared.Standing; 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.Events;
using Robust.Shared.Physics.Systems;
namespace Content.Shared.Slippery; namespace Content.Shared.Slippery;
public sealed class SlidingSystem : EntitySystem 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
_slipperyQuery = GetEntityQuery<SlipperyComponent>();
SubscribeLocalEvent<SlidingComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SlidingComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<SlidingComponent, StoodEvent>(OnStand); SubscribeLocalEvent<SlidingComponent, StoodEvent>(OnStand);
SubscribeLocalEvent<SlidingComponent, StartCollideEvent>(OnStartCollide); SubscribeLocalEvent<SlidingComponent, StartCollideEvent>(OnStartCollide);
SubscribeLocalEvent<SlidingComponent, EndCollideEvent>(OnEndCollide); 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>
@@ -25,28 +59,81 @@ public sealed class SlidingSystem : EntitySystem
} }
/// <summary> /// <summary>
/// Sets friction to 0 if colliding with a SuperSlippery Entity. /// Updates friction when we collide with a slippery entity
/// </summary> /// </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; return;
component.CollidingEntities.Add(args.OtherEntity); CalculateSlidingModifier(entity);
Dirty(uid, component); _speedModifierSystem.RefreshFrictionModifiers(entity);
} }
/// <summary> /// <summary>
/// Set friction to normal when ending collision with a SuperSlippery entity. /// Update friction when we stop colliding with a slippery entity
/// </summary> /// </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; return;
if (component.CollidingEntities.Count == 0) if (!CalculateSlidingModifier(entity, args.OtherEntity))
RemComp<SlidingComponent>(uid); {
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;
} }
} }

View File

@@ -21,6 +21,25 @@ namespace Content.Shared.Slippery
[Access(Other = AccessPermissions.ReadWriteExecute)] [Access(Other = AccessPermissions.ReadWriteExecute)]
public SoundSpecifier SlipSound = new SoundPathSpecifier("/Audio/Effects/slip.ogg"); 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> /// <summary>
/// Loads the data needed to determine how slippery something is. /// Loads the data needed to determine how slippery something is.
/// </summary> /// </summary>
@@ -34,10 +53,22 @@ namespace Content.Shared.Slippery
public sealed partial class SlipperyEffectEntry public sealed partial class SlipperyEffectEntry
{ {
/// <summary> /// <summary>
/// How many seconds the mob will be paralyzed for. /// How many seconds the mob will be stunned for.
/// </summary> /// </summary>
[DataField] [DataField]
public TimeSpan ParalyzeTime = TimeSpan.FromSeconds(1.5); 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 bool AutoStand = true;
/// <summary> /// <summary>
/// The entity's speed will be multiplied by this to slip it forwards. /// The entity's speed will be multiplied by this to slip it forwards.
@@ -63,6 +94,6 @@ namespace Content.Shared.Slippery
/// This is used to store the friction modifier that is used on a sliding entity. /// This is used to store the friction modifier that is used on a sliding entity.
/// </summary> /// </summary>
[DataField] [DataField]
public float SlipFriction; public float SlipFriction = 0.5f;
} }
} }

View File

@@ -1,4 +1,5 @@
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Damage.Systems;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -23,8 +24,10 @@ namespace Content.Shared.Slippery;
public sealed class SlipperySystem : EntitySystem public sealed class SlipperySystem : EntitySystem
{ {
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedStunSystem _stun = default!; [Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly SharedStaminaSystem _stamina = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!; [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
[Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!;
@@ -115,27 +118,21 @@ public sealed class SlipperySystem : EntitySystem
{ {
_physics.SetLinearVelocity(other, physics.LinearVelocity * component.SlipData.LaunchForwardsMultiplier, body: physics); _physics.SetLinearVelocity(other, physics.LinearVelocity * component.SlipData.LaunchForwardsMultiplier, body: physics);
if (component.SlipData.SuperSlippery && requiresContact) if (component.AffectsSliding && requiresContact)
{ EnsureComp<SlidingComponent>(other);
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));
}
} }
var playSound = !_statusEffects.HasStatusEffect(other, "KnockedDown"); // Preventing from playing the slip sound and stunning when you are already knocked down.
if (!HasComp<KnockedDownComponent>(other))
_stun.TryParalyze(other, component.SlipData.ParalyzeTime, true);
// Preventing from playing the slip sound when you are already knocked down.
if (playSound)
{ {
_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); _audio.PlayPredicted(component.SlipSound, other, other);
} }
_stun.TryKnockdown(other, component.SlipData.KnockdownTime, true, true);
_adminLogger.Add(LogType.Slip, LogImpact.Low, _adminLogger.Add(LogType.Slip, LogImpact.Low, $"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(uid):entity}");
$"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(uid):entity}");
} }
} }

View File

@@ -14,6 +14,25 @@ namespace Content.Shared.Standing
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public bool Standing { get; set; } = true; 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> /// <summary>
/// List of fixtures that had their collision mask changed when the entity was downed. /// List of fixtures that had their collision mask changed when the entity was downed.
/// Required for re-adding the collision mask. /// Required for re-adding the collision mask.

View File

@@ -1,4 +1,5 @@
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Physics; using Content.Shared.Physics;
using Content.Shared.Rotation; using Content.Shared.Rotation;
@@ -15,13 +16,16 @@ public sealed class StandingStateSystem : EntitySystem
[Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!;
// If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited. // 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<StandingStateComponent, AttemptMobCollideEvent>(OnMobCollide); SubscribeLocalEvent<StandingStateComponent, AttemptMobCollideEvent>(OnMobCollide);
SubscribeLocalEvent<StandingStateComponent, AttemptMobTargetCollideEvent>(OnMobTargetCollide); 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 OnMobTargetCollide(Entity<StandingStateComponent> ent, ref AttemptMobTargetCollideEvent args)
@@ -40,6 +44,27 @@ public sealed class StandingStateSystem : EntitySystem
} }
} }
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) public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null)
{ {
if (!Resolve(uid, ref standingState, false)) if (!Resolve(uid, ref standingState, false))

View File

@@ -1,3 +1,5 @@
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.StatusEffectNew.Components; using Content.Shared.StatusEffectNew.Components;
using Robust.Shared.Player; using Robust.Shared.Player;
@@ -9,6 +11,9 @@ public sealed partial class StatusEffectsSystem
{ {
SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerAttachedEvent>(RelayStatusEffectEvent); SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerAttachedEvent>(RelayStatusEffectEvent);
SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerDetachedEvent>(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 private void RefRelayStatusEffectEvent<T>(EntityUid uid, StatusEffectContainerComponent component, ref T args) where T : struct

View File

@@ -1,18 +1,46 @@
using Robust.Shared.Audio; using Content.Shared.DoAfter;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Stunnable; 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 public sealed partial class KnockedDownComponent : Component
{ {
[DataField("helpInterval"), AutoNetworkedField] /// <summary>
public float HelpInterval = 1f; /// Game time that we can stand up.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
public TimeSpan NextUpdate;
[DataField("helpAttemptSound")] /// <summary>
public SoundSpecifier StunAttemptSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"); /// Should we try to stand up?
/// </summary>
[DataField, AutoNetworkedField]
public bool AutoStand = true;
[ViewVariables, AutoNetworkedField] /// <summary>
public float HelpTimer = 0f; /// 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);
} }

View File

@@ -0,0 +1,538 @@
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
}

View File

@@ -1,12 +1,13 @@
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Interaction; using Content.Shared.Alert;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
using Content.Shared.Inventory.Events; using Content.Shared.Inventory.Events;
using Content.Shared.Item; using Content.Shared.Item;
using Content.Shared.Bed.Sleep;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Hands; using Content.Shared.Hands;
using Content.Shared.Mobs; using Content.Shared.Mobs;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
@@ -17,9 +18,7 @@ using Content.Shared.StatusEffect;
using Content.Shared.Throwing; using Content.Shared.Throwing;
using Content.Shared.Whitelist; using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Shared.Stunnable; namespace Content.Shared.Stunnable;
@@ -27,40 +26,28 @@ namespace Content.Shared.Stunnable;
public abstract partial class SharedStunSystem : EntitySystem public abstract partial class SharedStunSystem : EntitySystem
{ {
[Dependency] protected readonly ActionBlockerSystem Blocker = default!; [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 ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!; [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!; [Dependency] protected readonly SharedDoAfterSystem DoAfter = default!;
[Dependency] private readonly StandingStateSystem _standingState = default!; [Dependency] protected readonly SharedStaminaSystem Stamina = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffect = 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() public override void Initialize()
{ {
SubscribeLocalEvent<KnockedDownComponent, ComponentInit>(OnKnockInit);
SubscribeLocalEvent<KnockedDownComponent, ComponentShutdown>(OnKnockShutdown);
SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandAttempt);
SubscribeLocalEvent<SlowedDownComponent, ComponentInit>(OnSlowInit); SubscribeLocalEvent<SlowedDownComponent, ComponentInit>(OnSlowInit);
SubscribeLocalEvent<SlowedDownComponent, ComponentShutdown>(OnSlowRemove); SubscribeLocalEvent<SlowedDownComponent, ComponentShutdown>(OnSlowRemove);
SubscribeLocalEvent<SlowedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
SubscribeLocalEvent<StunnedComponent, ComponentStartup>(UpdateCanMove); SubscribeLocalEvent<StunnedComponent, ComponentStartup>(UpdateCanMove);
SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(OnStunShutdown); SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(OnStunShutdown);
SubscribeLocalEvent<StunOnContactComponent, StartCollideEvent>(OnStunOnContactCollide); 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. // Attempt event subscriptions.
SubscribeLocalEvent<StunnedComponent, ChangeDirectionAttemptEvent>(OnAttempt); SubscribeLocalEvent<StunnedComponent, ChangeDirectionAttemptEvent>(OnAttempt);
SubscribeLocalEvent<StunnedComponent, UpdateCanMoveEvent>(OnMoveAttempt); SubscribeLocalEvent<StunnedComponent, UpdateCanMoveEvent>(OnMoveAttempt);
@@ -74,7 +61,7 @@ public abstract partial class SharedStunSystem : EntitySystem
SubscribeLocalEvent<StunnedComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt); SubscribeLocalEvent<StunnedComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
SubscribeLocalEvent<MobStateComponent, MobStateChangedEvent>(OnMobStateChanged); SubscribeLocalEvent<MobStateComponent, MobStateChangedEvent>(OnMobStateChanged);
// Stun Appearance Data InitializeKnockdown();
InitializeAppearance(); InitializeAppearance();
} }
@@ -136,23 +123,7 @@ public abstract partial class SharedStunSystem : EntitySystem
return; return;
TryStun(args.OtherEntity, ent.Comp.Duration, true, status); TryStun(args.OtherEntity, ent.Comp.Duration, true, status);
TryKnockdown(args.OtherEntity, ent.Comp.Duration, true, status); TryKnockdown(args.OtherEntity, ent.Comp.Duration, ent.Comp.Refresh, ent.Comp.AutoStand);
}
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();
} }
private void OnSlowInit(EntityUid uid, SlowedDownComponent component, ComponentInit args) private void OnSlowInit(EntityUid uid, SlowedDownComponent component, ComponentInit args)
@@ -167,18 +138,12 @@ public abstract partial class SharedStunSystem : EntitySystem
_movementSpeedModifier.RefreshMovementSpeedModifiers(uid); _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...) // TODO STUN: Make events for different things. (Getting modifiers, attempt events, informative events...)
/// <summary> /// <summary>
/// Stuns the entity, disallowing it from doing many interactions temporarily. /// Stuns the entity, disallowing it from doing many interactions temporarily.
/// </summary> /// </summary>
public bool TryStun(EntityUid uid, TimeSpan time, bool refresh, public bool TryStun(EntityUid uid, TimeSpan time, bool refresh, StatusEffectsComponent? status = null)
StatusEffectsComponent? status = null)
{ {
if (time <= TimeSpan.Zero) if (time <= TimeSpan.Zero)
return false; return false;
@@ -199,20 +164,48 @@ public abstract partial class SharedStunSystem : EntitySystem
/// <summary> /// <summary>
/// Knocks down the entity, making it fall to the ground. /// Knocks down the entity, making it fall to the ground.
/// </summary> /// </summary>
public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh, public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh, bool autoStand = true, bool drop = true)
StatusEffectsComponent? status = null)
{ {
if (time <= TimeSpan.Zero) if (time <= TimeSpan.Zero)
return false; 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; 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; return false;
var ev = new KnockedDownEvent(); // Initialize our component with the relevant data we need if we don't have it
RaiseLocalEvent(uid, ref ev); 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; return true;
} }
@@ -226,14 +219,14 @@ public abstract partial class SharedStunSystem : EntitySystem
if (!Resolve(uid, ref status, false)) if (!Resolve(uid, ref status, false))
return 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> /// <summary>
/// Slows down the mob's walking/running speed temporarily /// Slows down the mob's walking/running speed temporarily
/// </summary> /// </summary>
public bool TrySlowdown(EntityUid uid, TimeSpan time, bool refresh, public bool TrySlowdown(EntityUid uid, TimeSpan time, bool refresh,
float walkSpeedMultiplier = 1f, float runSpeedMultiplier = 1f, float walkSpeedMod = 1f, float sprintSpeedMod = 1f,
StatusEffectsComponent? status = null) StatusEffectsComponent? status = null)
{ {
if (!Resolve(uid, ref status, false)) if (!Resolve(uid, ref status, false))
@@ -246,11 +239,11 @@ public abstract partial class SharedStunSystem : EntitySystem
{ {
var slowed = Comp<SlowedDownComponent>(uid); var slowed = Comp<SlowedDownComponent>(uid);
// Doesn't make much sense to have the "TrySlowdown" method speed up entities now does it? // Doesn't make much sense to have the "TrySlowdown" method speed up entities now does it?
walkSpeedMultiplier = Math.Clamp(walkSpeedMultiplier, 0f, 1f); walkSpeedMod = Math.Clamp(walkSpeedMod, 0f, 1f);
runSpeedMultiplier = Math.Clamp(runSpeedMultiplier, 0f, 1f); sprintSpeedMod = Math.Clamp(sprintSpeedMod, 0f, 1f);
slowed.WalkSpeedModifier *= walkSpeedMultiplier; slowed.WalkSpeedModifier *= walkSpeedMod;
slowed.SprintSpeedModifier *= runSpeedMultiplier; slowed.SprintSpeedModifier *= sprintSpeedMod;
_movementSpeedModifier.RefreshMovementSpeedModifiers(uid); _movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
return true; return true;
@@ -310,29 +303,14 @@ public abstract partial class SharedStunSystem : EntitySystem
UpdateStunModifiers(ent, speedModifier, speedModifier); UpdateStunModifiers(ent, speedModifier, speedModifier);
} }
private void OnInteractHand(EntityUid uid, KnockedDownComponent knocked, InteractHandEvent args) #region friction and movement listeners
private void OnRefreshMovespeed(EntityUid ent, SlowedDownComponent comp, RefreshMovementSpeedModifiersEvent args)
{ {
if (args.Handled || knocked.HelpTimer > 0f) args.ModifySpeed(comp.WalkSpeedModifier, comp.SprintSpeedModifier);
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);
args.Handled = true;
} }
private void OnKnockedTileFriction(EntityUid uid, KnockedDownComponent component, ref TileFrictionEvent args) #endregion
{
args.Modifier *= KnockDownModifier;
}
#region Attempt Event Handling #region Attempt Event Handling
@@ -365,15 +343,3 @@ public abstract partial class SharedStunSystem : EntitySystem
#endregion #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;

View File

@@ -18,6 +18,18 @@ public sealed partial class StunOnContactComponent : Component
[DataField] [DataField]
public TimeSpan Duration = TimeSpan.FromSeconds(5); 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] [DataField]
public EntityWhitelist Blacklist = new(); public EntityWhitelist Blacklist = new();
} }

View File

@@ -0,0 +1,89 @@
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;

View File

@@ -25,4 +25,13 @@
/// Raised when we try to pushback an entity from throwing /// Raised when we try to pushback an entity from throwing
/// </summary> /// </summary>
public sealed class ThrowPushbackAttemptEvent : CancellableEntityEventArgs {} 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;
};
} }

View File

@@ -222,17 +222,21 @@ public sealed class ThrowingSystem : EntitySystem
_recoil.KickCamera(user.Value, -direction * 0.04f); _recoil.KickCamera(user.Value, -direction * 0.04f);
// Give thrower an impulse in the other direction // Give thrower an impulse in the other direction
if (pushbackRatio != 0.0f && if (pushbackRatio == 0.0f ||
physics.Mass > 0f && physics.Mass == 0f ||
TryComp(user.Value, out PhysicsComponent? userPhysics) && !TryComp(user.Value, out PhysicsComponent? userPhysics))
_gravity.IsWeightless(user.Value, userPhysics)) return;
{ var msg = new ThrowPushbackAttemptEvent();
var msg = new ThrowPushbackAttemptEvent(); RaiseLocalEvent(uid, msg);
RaiseLocalEvent(uid, msg);
const float massLimit = 5f;
if (!msg.Cancelled) if (msg.Cancelled)
_physics.ApplyLinearImpulse(user.Value, -impulseVector / physics.Mass * pushbackRatio * MathF.Min(massLimit, physics.Mass), body: userPhysics); 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);
} }
} }

View File

@@ -384,11 +384,14 @@ public abstract partial class SharedGunSystem : EntitySystem
var shotEv = new GunShotEvent(user, ev.Ammo); var shotEv = new GunShotEvent(user, ev.Ammo);
RaiseLocalEvent(gunUid, ref shotEv); RaiseLocalEvent(gunUid, ref shotEv);
if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics)) if (!userImpulse || !TryComp<PhysicsComponent>(user, out var userPhysics))
{ return;
if (_gravity.IsWeightless(user, userPhysics))
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics); var shooterEv = new ShooterImpulseEvent();
} RaiseLocalEvent(user, ref shooterEv);
if (shooterEv.Push || _gravity.IsWeightless(user, userPhysics))
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
} }
public void Shoot( public void Shoot(
@@ -629,6 +632,16 @@ public record struct AttemptShootEvent(EntityUid User, string? Message, bool Can
[ByRefEvent] [ByRefEvent]
public record struct GunShotEvent(EntityUid User, List<(EntityUid? Uid, IShootable Shootable)> Ammo); 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 public enum EffectLayers : byte
{ {
Unshaded, Unshaded,

View File

@@ -57,6 +57,7 @@ admin-smite-ghostkick-name = Ghost Kick
admin-smite-nyanify-name = Cat Ears admin-smite-nyanify-name = Cat Ears
admin-smite-kill-sign-name = Kill Sign admin-smite-kill-sign-name = Kill Sign
admin-smite-omni-accent-name = Omni-Accent admin-smite-omni-accent-name = Omni-Accent
admin-smite-crawler-name = Crawler
## Smite descriptions ## Smite descriptions
@@ -101,6 +102,7 @@ admin-smite-super-bonk-lite-description= Slams them on every single table on the
admin-smite-terminate-description = Creates a Terminator ghost role with the sole objective of killing them. 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-super-slip-description = Slips them really, really hard.
admin-smite-omni-accent-description = Makes the target speak with almost every accent available. 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 ## Tricks descriptions

View File

@@ -33,6 +33,9 @@ alerts-walking-desc = You are walking, moving at a slow pace.
alerts-stunned-name = [color=yellow]Stunned[/color] 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-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-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. 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.

View File

@@ -136,6 +136,7 @@ ui-options-function-move-left = Move Left
ui-options-function-move-down = Move Down ui-options-function-move-down = Move Down
ui-options-function-move-right = Move Right ui-options-function-move-right = Move Right
ui-options-function-walk = Walk 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-left = Rotate left
ui-options-function-camera-rotate-right = Rotate right ui-options-function-camera-rotate-right = Rotate right

View File

@@ -1,2 +1,6 @@
stunnable-component-disarm-success-others = {CAPITALIZE(THE($source))} pushes {THE($target)}! stunnable-component-disarm-success-others = {CAPITALIZE(THE($source))} pushes {THE($target)}!
stunnable-component-disarm-success = You push {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!

View File

@@ -146,6 +146,15 @@
name: alerts-stunned-name name: alerts-stunned-name
description: alerts-stunned-desc 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 - type: alert
id: Handcuffed id: Handcuffed
clickEvent: !type:RemoveCuffsAlertEvent clickEvent: !type:RemoveCuffsAlertEvent

View File

@@ -159,6 +159,8 @@
components: components:
- type: Clickable - type: Clickable
- type: Slippery - type: Slippery
staminaDamage: 0
frictionStatusTime: 0 # Don't apply friction twice
- type: Transform - type: Transform
noRot: true noRot: true
anchored: true anchored: true

View File

@@ -144,7 +144,6 @@
- type: StatusEffects - type: StatusEffects
allowed: allowed:
- Stun - Stun
- KnockedDown
- SlowedDown - SlowedDown
- Flashed - Flashed
- type: TypingIndicator - type: TypingIndicator

View File

@@ -36,7 +36,6 @@
- type: StatusEffects - type: StatusEffects
allowed: allowed:
- Stun - Stun
- KnockedDown
- SlowedDown - SlowedDown
- Stutter - Stutter
- Electrocution - Electrocution

View File

@@ -16,7 +16,6 @@
- type: StatusEffects - type: StatusEffects
allowed: allowed:
- Stun - Stun
- KnockedDown
- SlowedDown - SlowedDown
- Stutter - Stutter
- Electrocution - Electrocution

View File

@@ -94,7 +94,6 @@
- type: StatusEffects - type: StatusEffects
allowed: allowed:
- Stun - Stun
- KnockedDown
- SlowedDown - SlowedDown
- Stutter - Stutter
- Electrocution - Electrocution

View File

@@ -120,7 +120,7 @@
- type: StatusEffects - type: StatusEffects
allowed: allowed:
- Stun - Stun
- KnockedDown - Friction
- SlowedDown - SlowedDown
- Stutter - Stutter
- Electrocution - Electrocution

View File

@@ -81,6 +81,26 @@
- type: TrashOnSolutionEmpty - type: TrashOnSolutionEmpty
solution: drink 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 # Mopwata
- type: weightedRandomFillSolution - type: weightedRandomFillSolution

View File

@@ -17,7 +17,7 @@
Quantity: 5 Quantity: 5
- type: Slippery - type: Slippery
slipData: slipData:
paralyzeTime: 3 knockdownTime: 3
launchForwardsMultiplier: 3 launchForwardsMultiplier: 3
- type: StepTrigger - type: StepTrigger
intersectRatio: 0.2 intersectRatio: 0.2

View File

@@ -872,8 +872,10 @@
- type: UseDelay - type: UseDelay
delay: 0.8 delay: 0.8
- type: Slippery - type: Slippery
staminaDamage: 0
slipData: slipData:
paralyzeTime: 0 stunTime: 0
knockdownTime: 0
launchForwardsMultiplier: 0 launchForwardsMultiplier: 0
slipSound: slipSound:
collection: Parp collection: Parp

View File

@@ -127,8 +127,9 @@
- type: SolutionContainerVisuals - type: SolutionContainerVisuals
fillBaseName: syndie- fillBaseName: syndie-
- type: Slippery - type: Slippery
staminaDamage: 50
slipData: slipData:
paralyzeTime: 3 knockdownTime: 3
launchForwardsMultiplier: 3 launchForwardsMultiplier: 3
- type: Item - type: Item
heldPrefix: syndie heldPrefix: syndie
@@ -155,9 +156,6 @@
layers: layers:
- state: syndie-soaplet - state: syndie-soaplet
- type: Slippery - type: Slippery
slipData:
paralyzeTime: 1.5 # these things are tiny
launchForwardsMultiplier: 1.5
- type: StepTrigger - type: StepTrigger
intersectRatio: 0.04 intersectRatio: 0.04
- type: Item - type: Item
@@ -222,8 +220,9 @@
- type: SolutionContainerVisuals - type: SolutionContainerVisuals
fillBaseName: omega- fillBaseName: omega-
- type: Slippery - type: Slippery
staminaDamage: 50
slipData: slipData:
paralyzeTime: 5.0 knockdownTime: 5.0
launchForwardsMultiplier: 3.0 launchForwardsMultiplier: 3.0
- type: Item - type: Item
heldPrefix: omega heldPrefix: omega

View File

@@ -547,7 +547,7 @@
name: taser name: taser
parent: [BaseWeaponBatterySmall, BaseSecurityContraband] parent: [BaseWeaponBatterySmall, BaseSecurityContraband]
id: WeaponTaser 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: components:
- type: Tag - type: Tag
tags: tags:
@@ -579,6 +579,23 @@
zeroVisible: true zeroVisible: true
- type: Appearance - 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 - type: entity
name: antique laser pistol name: antique laser pistol
parent: [BaseWeaponBatterySmall, BaseGrandTheftContraband] parent: [BaseWeaponBatterySmall, BaseGrandTheftContraband]

View File

@@ -216,13 +216,46 @@
- type: Projectile - type: Projectile
damage: damage:
types: types:
Heat: 5 Shock: 1
soundHit: soundHit:
path: "/Audio/Weapons/Guns/Hits/taser_hit.ogg" path: "/Audio/Weapons/Guns/Hits/taser_hit.ogg"
forceSound: true 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 - type: StunOnCollide
stunAmount: 5 stunAmount: 5
knockdownAmount: 5 knockdownAmount: 10
slowdownAmount: 10
walkSpeedModifier: 0.5
sprintSpeedModifier: 0.5
- type: entity - type: entity
name : disabler bolt name : disabler bolt

View File

@@ -43,10 +43,18 @@
components: components:
- type: DrowsinessStatusEffect - type: DrowsinessStatusEffect
# Makes you more slippery, or perhaps less slippery.
- type: entity
parent: MobStatusEffectBase
id: StatusEffectFriction
name: friction
components:
- type: FrictionStatusEffect
# Adds drugs overlay # Adds drugs overlay
- type: entity - type: entity
parent: MobStatusEffectBase parent: MobStatusEffectBase
id: StatusEffectSeeingRainbow id: StatusEffectSeeingRainbow
name: hallucinations name: hallucinations
components: components:
- type: SeeingRainbowsStatusEffect - type: SeeingRainbowsStatusEffect

View File

@@ -83,7 +83,7 @@
meltingPoint: 18.2 meltingPoint: 18.2
tileReactions: tileReactions:
- !type:SpillTileReaction - !type:SpillTileReaction
friction: 0.0 friction: 0.05
- type: reagent - type: reagent
id: SpaceGlue id: SpaceGlue

View File

@@ -8,10 +8,6 @@
id: Stun id: Stun
alert: Stun alert: Stun
- type: statusEffect
id: KnockedDown
alert: Stun
- type: statusEffect - type: statusEffect
id: SlowedDown id: SlowedDown

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1,14 @@
{
"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"
}
]
}

View File

@@ -190,7 +190,7 @@ binds:
mod1: Alt mod1: Alt
- function: OpenCharacterMenu - function: OpenCharacterMenu
type: State type: State
key: C key: U
- function: OpenEmotesMenu - function: OpenEmotesMenu
type: State type: State
key: Y key: Y
@@ -525,6 +525,9 @@ binds:
- function: Arcade3 - function: Arcade3
type: State type: State
key: Z key: Z
- function: ToggleKnockdown
type: State
key: C
- function: OpenAbilitiesMenu - function: OpenAbilitiesMenu
type: State type: State
key: K key: K