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:
committed by
GitHub
parent
cfb0a95035
commit
dec2d42a1d
@@ -55,6 +55,7 @@ namespace Content.Client.Input
|
||||
human.AddFunction(EngineKeyFunctions.MoveLeft);
|
||||
human.AddFunction(EngineKeyFunctions.MoveRight);
|
||||
human.AddFunction(EngineKeyFunctions.Walk);
|
||||
human.AddFunction(ContentKeyFunctions.ToggleKnockdown);
|
||||
human.AddFunction(ContentKeyFunctions.SwapHands);
|
||||
human.AddFunction(ContentKeyFunctions.SwapHandsReverse);
|
||||
human.AddFunction(ContentKeyFunctions.Drop);
|
||||
|
||||
@@ -162,6 +162,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
AddButton(EngineKeyFunctions.Walk);
|
||||
AddCheckBox("ui-options-hotkey-toggle-walk", _cfg.GetCVar(CCVars.ToggleWalk), HandleToggleWalk);
|
||||
InitToggleWalk();
|
||||
AddButton(ContentKeyFunctions.ToggleKnockdown);
|
||||
|
||||
AddHeader("ui-options-header-camera");
|
||||
AddButton(EngineKeyFunctions.CameraRotateLeft);
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Mobs;
|
||||
using System.Numerics;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Stunnable;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Stunnable;
|
||||
|
||||
public sealed class StunSystem : SharedStunSystem
|
||||
{
|
||||
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
|
||||
|
||||
@@ -23,6 +25,22 @@ public sealed class StunSystem : SharedStunSystem
|
||||
|
||||
SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<StunVisualsComponent, AppearanceChangeEvent>(OnAppearanceChanged);
|
||||
|
||||
CommandBinds.Builder
|
||||
.BindAfter(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnUseSecondary, true, true), typeof(SharedInteractionSystem))
|
||||
.Register<StunSystem>();
|
||||
}
|
||||
|
||||
private bool OnUseSecondary(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (args.Session?.AttachedEntity is not {Valid: true} uid)
|
||||
return false;
|
||||
|
||||
if (args.EntityUid != uid || !HasComp<KnockedDownComponent>(uid) || !_combat.IsInCombatMode(uid))
|
||||
return false;
|
||||
|
||||
RaisePredictiveEvent(new ForceStandUpEvent());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -39,6 +39,7 @@ using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Slippery;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Tabletop.Components;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -48,6 +49,7 @@ using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
@@ -877,7 +879,7 @@ public sealed partial class AdminVerbSystem
|
||||
if (!hadSlipComponent)
|
||||
{
|
||||
slipComponent.SlipData.SuperSlippery = true;
|
||||
slipComponent.SlipData.ParalyzeTime = TimeSpan.FromSeconds(5);
|
||||
slipComponent.SlipData.StunTime = TimeSpan.FromSeconds(5);
|
||||
slipComponent.SlipData.LaunchForwardsMultiplier = 20;
|
||||
}
|
||||
|
||||
@@ -922,5 +924,20 @@ public sealed partial class AdminVerbSystem
|
||||
Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
|
||||
};
|
||||
args.Verbs.Add(omniaccent);
|
||||
|
||||
var crawlerName = Loc.GetString("admin-smite-crawler-name").ToLowerInvariant();
|
||||
Verb crawler = new()
|
||||
{
|
||||
Text = crawlerName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("Mobs/Animals/snake.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
EnsureComp<WormComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", crawlerName, Loc.GetString("admin-smite-crawler-description"))
|
||||
};
|
||||
args.Verbs.Add(crawler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,6 +338,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
// Ensure we actually have the component
|
||||
EnsureComp<TileFrictionModifierComponent>(entity);
|
||||
|
||||
EnsureComp<SlipperyComponent>(entity, out var slipComp);
|
||||
|
||||
// This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on
|
||||
// the sprite threshold for a puddle larger than 5 pixels.
|
||||
var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
|
||||
@@ -356,17 +358,21 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
var launchMult = FixedPoint2.Zero;
|
||||
// A cumulative weighted amount of stun times from slippery reagents
|
||||
var stunTimer = TimeSpan.Zero;
|
||||
// A cumulative weighted amount of knockdown times from slippery reagents
|
||||
var knockdownTimer = TimeSpan.Zero;
|
||||
|
||||
// Check if the puddle is big enough to slip in to avoid doing unnecessary logic
|
||||
if (solution.Volume <= smallPuddleThreshold)
|
||||
{
|
||||
_stepTrigger.SetActive(entity, false, comp);
|
||||
_tile.SetModifier(entity, 1f);
|
||||
slipComp.SlipData.SlipFriction = 1f;
|
||||
slipComp.AffectsSliding = false;
|
||||
Dirty(entity, slipComp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<SlipperyComponent>(entity, out var slipComp))
|
||||
return;
|
||||
slipComp.AffectsSliding = true;
|
||||
|
||||
foreach (var (reagent, quantity) in solution.Contents)
|
||||
{
|
||||
@@ -386,7 +392,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
// Aggregate launch speed based on quantity
|
||||
launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity;
|
||||
// Aggregate stun times based on quantity
|
||||
stunTimer += reagentProto.SlipData.ParalyzeTime * (float)quantity;
|
||||
stunTimer += reagentProto.SlipData.StunTime * (float)quantity;
|
||||
knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity;
|
||||
|
||||
if (reagentProto.SlipData.SuperSlippery)
|
||||
superSlipperyUnits += quantity;
|
||||
@@ -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.
|
||||
if (slipperyUnits > 0)
|
||||
{
|
||||
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits);
|
||||
slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits;
|
||||
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
|
||||
slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits);
|
||||
slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits);
|
||||
}
|
||||
|
||||
// Only make it super slippery if there is enough super slippery units for its own puddle
|
||||
|
||||
@@ -8,21 +8,47 @@ namespace Content.Server.Stunnable.Components
|
||||
{
|
||||
// TODO: Can probably predict this.
|
||||
|
||||
// See stunsystem for what these do
|
||||
[DataField("stunAmount")]
|
||||
public int StunAmount;
|
||||
/// <summary>
|
||||
/// How long we are stunned for
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan StunAmount;
|
||||
|
||||
[DataField("knockdownAmount")]
|
||||
public int KnockdownAmount;
|
||||
/// <summary>
|
||||
/// How long we are knocked down for
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan KnockdownAmount;
|
||||
|
||||
[DataField("slowdownAmount")]
|
||||
public int SlowdownAmount;
|
||||
/// <summary>
|
||||
/// How long we are slowed down for
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan SlowdownAmount;
|
||||
|
||||
[DataField("walkSpeedMultiplier")]
|
||||
public float WalkSpeedMultiplier = 1f;
|
||||
/// <summary>
|
||||
/// Multiplier for a mob's walking speed
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float WalkSpeedModifier = 1f;
|
||||
|
||||
[DataField("runSpeedMultiplier")]
|
||||
public float RunSpeedMultiplier = 1f;
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// Fixture we track for the collision.
|
||||
|
||||
6
Content.Server/Stunnable/StunSystem.cs
Normal file
6
Content.Server/Stunnable/StunSystem.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using Content.Shared.Stunnable;
|
||||
|
||||
namespace Content.Server.Stunnable;
|
||||
|
||||
public sealed class StunSystem : SharedStunSystem;
|
||||
|
||||
@@ -22,17 +22,14 @@ namespace Content.Server.Stunnable
|
||||
|
||||
private void TryDoCollideStun(EntityUid uid, StunOnCollideComponent component, EntityUid target)
|
||||
{
|
||||
if (!TryComp<StatusEffectsComponent>(target, out var status))
|
||||
return;
|
||||
|
||||
if (TryComp<StatusEffectsComponent>(target, out var status))
|
||||
{
|
||||
_stunSystem.TryStun(target, TimeSpan.FromSeconds(component.StunAmount), true, status);
|
||||
_stunSystem.TryStun(target, component.StunAmount, component.Refresh, status);
|
||||
|
||||
_stunSystem.TryKnockdown(target, TimeSpan.FromSeconds(component.KnockdownAmount), true,
|
||||
status);
|
||||
_stunSystem.TryKnockdown(target, component.KnockdownAmount, component.Refresh, component.AutoStand);
|
||||
|
||||
_stunSystem.TrySlowdown(target, TimeSpan.FromSeconds(component.SlowdownAmount), true,
|
||||
component.WalkSpeedMultiplier, component.RunSpeedMultiplier, status);
|
||||
}
|
||||
_stunSystem.TrySlowdown(target, component.SlowdownAmount, component.Refresh, component.WalkSpeedModifier, component.SprintSpeedModifier, status);
|
||||
}
|
||||
private void HandleCollide(EntityUid uid, StunOnCollideComponent component, ref StartCollideEvent args)
|
||||
{
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using Content.Shared.Stunnable;
|
||||
|
||||
namespace Content.Server.Stunnable
|
||||
{
|
||||
public sealed class StunSystem : SharedStunSystem
|
||||
{}
|
||||
}
|
||||
@@ -108,7 +108,6 @@ public sealed partial class SleepingSystem : EntitySystem
|
||||
{
|
||||
// Expiring status effects would remove the components needed for sleeping
|
||||
_statusEffectOld.TryRemoveStatusEffect(ent.Owner, "Stun");
|
||||
_statusEffectOld.TryRemoveStatusEffect(ent.Owner, "KnockedDown");
|
||||
|
||||
EnsureComp<StunnedComponent>(ent);
|
||||
EnsureComp<KnockedDownComponent>(ent);
|
||||
|
||||
@@ -33,6 +33,24 @@ public sealed partial class HandcuffComponent : Component
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float StunBonus = 2f;
|
||||
|
||||
/// <summary>
|
||||
/// Modifier for the amount of time it takes an entity to stand up if cuffed.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float StandupMod = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Modifier to the speed of an entity who is cuffed, does not stack with KnockedMovementMod
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MovementMod = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Modifier to the knocked down speed of an entity who is cuffed
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float KnockedMovementMod = 0.4f;
|
||||
|
||||
/// <summary>
|
||||
/// Will the cuffs break when removed?
|
||||
/// </summary>
|
||||
|
||||
@@ -21,6 +21,7 @@ using Content.Shared.Inventory.VirtualItem;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Pulling.Events;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Pulling.Events;
|
||||
using Content.Shared.Rejuvenate;
|
||||
@@ -45,6 +46,7 @@ namespace Content.Shared.Cuffs
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _move = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
@@ -56,10 +58,14 @@ namespace Content.Shared.Cuffs
|
||||
[Dependency] private readonly UseDelaySystem _delay = default!;
|
||||
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
|
||||
|
||||
private EntityQuery<HandcuffComponent> _cuffQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_cuffQuery = GetEntityQuery<HandcuffComponent>();
|
||||
|
||||
SubscribeLocalEvent<CuffableComponent, HandCountChangedEvent>(OnHandCountChanged);
|
||||
SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
|
||||
|
||||
@@ -89,6 +95,9 @@ namespace Content.Shared.Cuffs
|
||||
SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
|
||||
SubscribeLocalEvent<HandcuffComponent, AddCuffDoAfterEvent>(OnAddCuffDoAfter);
|
||||
SubscribeLocalEvent<HandcuffComponent, VirtualItemDeletedEvent>(OnCuffVirtualItemDeleted);
|
||||
SubscribeLocalEvent<CuffableComponent, GetStandUpTimeEvent>(OnCuffableStandupArgs);
|
||||
SubscribeLocalEvent<CuffableComponent, KnockedDownRefreshEvent>(OnCuffableKnockdownRefresh);
|
||||
SubscribeLocalEvent<CuffableComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
|
||||
}
|
||||
|
||||
private void CheckInteract(Entity<CuffableComponent> ent, ref InteractionAttemptEvent args)
|
||||
@@ -366,6 +375,9 @@ namespace Content.Shared.Cuffs
|
||||
_adminLog.Add(LogType.Action, LogImpact.High,
|
||||
$"{ToPrettyString(user):player} has cuffed {ToPrettyString(target):player}");
|
||||
}
|
||||
|
||||
if (!MathHelper.CloseTo(component.MovementMod, 1f))
|
||||
_move.RefreshMovementSpeedModifiers(target);
|
||||
}
|
||||
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>
|
||||
/// Adds virtual cuff items to the user's hands.
|
||||
/// </summary>
|
||||
@@ -736,6 +814,9 @@ namespace Content.Shared.Cuffs
|
||||
shoved = true;
|
||||
}
|
||||
|
||||
if (!MathHelper.CloseTo(cuff.MovementMod, 1f))
|
||||
_move.RefreshMovementSpeedModifiers(target);
|
||||
|
||||
if (cuffable.CuffedHandCount == 0)
|
||||
{
|
||||
if (user != null)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
@@ -38,7 +39,7 @@ public sealed partial class StaminaComponent : Component
|
||||
public float StaminaDamage;
|
||||
|
||||
/// <summary>
|
||||
/// How much stamina damage is required to entire stam crit.
|
||||
/// How much stamina damage is required to enter stam crit.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
public float CritThreshold = 100f;
|
||||
@@ -71,6 +72,18 @@ public sealed partial class StaminaComponent : Component
|
||||
[DataField, AutoNetworkedField]
|
||||
public float AfterCritDecayMultiplier = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// This is how much stamina damage a mob takes when it forces itself to stand up before modifiers
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float ForceStandStamina = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// What sound should play when we successfully stand up
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public SoundSpecifier ForceStandSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Thresholds that determine an entity's slowdown as a function of stamina damage.
|
||||
/// </summary>
|
||||
|
||||
@@ -234,7 +234,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Tries to take stamina damage without raising the entity over the crit threshold.
|
||||
/// </summary>
|
||||
public bool TryTakeStamina(EntityUid uid, float value, StaminaComponent? component = null, EntityUid? source = null, EntityUid? with = null)
|
||||
public bool TryTakeStamina(EntityUid uid, float value, StaminaComponent? component = null, EntityUid? source = null, EntityUid? with = null, bool visual = false)
|
||||
{
|
||||
// Something that has no Stamina component automatically passes stamina checks
|
||||
if (!Resolve(uid, ref component, false))
|
||||
@@ -242,10 +242,10 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
|
||||
var oldStam = component.StaminaDamage;
|
||||
|
||||
if (oldStam + value > component.CritThreshold || component.Critical)
|
||||
if (oldStam + value >= component.CritThreshold || component.Critical)
|
||||
return false;
|
||||
|
||||
TakeStaminaDamage(uid, value, component, source, with, visual: false);
|
||||
TakeStaminaDamage(uid, value, component, source, with, visual: visual);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public abstract partial class SharedHandsSystem
|
||||
InitializeDrop();
|
||||
InitializePickup();
|
||||
InitializeRelay();
|
||||
InitializeEventListeners();
|
||||
|
||||
SubscribeLocalEvent<HandsComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<HandsComponent, MapInitEvent>(OnMapInit);
|
||||
@@ -166,6 +167,26 @@ public abstract partial class SharedHandsSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does this entity have any empty hands, and how many?
|
||||
/// </summary>
|
||||
public int GetEmptyHandCount(Entity<HandsComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp, false) || entity.Comp.Count == 0)
|
||||
return 0;
|
||||
|
||||
var hands = 0;
|
||||
|
||||
foreach (var hand in EnumerateHands(entity))
|
||||
{
|
||||
if (!HandIsEmpty(entity, hand))
|
||||
continue;
|
||||
hands++;
|
||||
}
|
||||
|
||||
return hands;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the item held in the entity's active hand.
|
||||
/// </summary>
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Content.Shared.Input
|
||||
[KeyFunctions]
|
||||
public static class ContentKeyFunctions
|
||||
{
|
||||
public static readonly BoundKeyFunction ToggleKnockdown = "ToggleKnockdown";
|
||||
public static readonly BoundKeyFunction UseItemInHand = "ActivateItemInHand";
|
||||
public static readonly BoundKeyFunction AltUseItemInHand = "AltActivateItemInHand";
|
||||
public static readonly BoundKeyFunction ActivateItemInWorld = "ActivateItemInWorld";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
23
Content.Shared/Movement/Components/WormComponent.cs
Normal file
23
Content.Shared/Movement/Components/WormComponent.cs
Normal 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;
|
||||
}
|
||||
100
Content.Shared/Movement/Systems/MovementModStatusSystem.cs
Normal file
100
Content.Shared/Movement/Systems/MovementModStatusSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Standing;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -11,7 +10,7 @@ namespace Content.Shared.Movement.Systems
|
||||
public sealed class MovementSpeedModifierSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
|
||||
private float _frictionModifier;
|
||||
private float _airDamping;
|
||||
@@ -21,6 +20,8 @@ namespace Content.Shared.Movement.Systems
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<MovementSpeedModifierComponent, MapInitEvent>(OnModMapInit);
|
||||
SubscribeLocalEvent<MovementSpeedModifierComponent, DownedEvent>(OnDowned);
|
||||
SubscribeLocalEvent<MovementSpeedModifierComponent, StoodEvent>(OnStand);
|
||||
|
||||
Subs.CVar(_configManager, CCVars.TileFrictionModifier, value => _frictionModifier = value, true);
|
||||
Subs.CVar(_configManager, CCVars.AirFriction, value => _airDamping = value, true);
|
||||
@@ -41,6 +42,18 @@ namespace Content.Shared.Movement.Systems
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnDowned(Entity<MovementSpeedModifierComponent> entity, ref DownedEvent args)
|
||||
{
|
||||
RefreshFrictionModifiers(entity);
|
||||
RefreshMovementSpeedModifiers(entity);
|
||||
}
|
||||
|
||||
private void OnStand(Entity<MovementSpeedModifierComponent> entity, ref StoodEvent args)
|
||||
{
|
||||
RefreshFrictionModifiers(entity);
|
||||
RefreshMovementSpeedModifiers(entity);
|
||||
}
|
||||
|
||||
public void RefreshWeightlessModifiers(EntityUid uid, MovementSpeedModifierComponent? move = null)
|
||||
{
|
||||
if (!Resolve(uid, ref move, false))
|
||||
|
||||
53
Content.Shared/Movement/Systems/WormSystem.cs
Normal file
53
Content.Shared/Movement/Systems/WormSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,15 @@ namespace Content.Shared.Slippery;
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
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>
|
||||
/// The friction modifier that will be applied to any friction calculations.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float FrictionModifier;
|
||||
|
||||
/// <summary>
|
||||
/// Hashset of contacting entities.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<EntityUid> Contacting = new();
|
||||
}
|
||||
|
||||
@@ -1,19 +1,53 @@
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Standing;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Weapons.Ranged.Systems;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
|
||||
namespace Content.Shared.Slippery;
|
||||
|
||||
public sealed class SlidingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _speedModifierSystem = default!;
|
||||
|
||||
private EntityQuery<SlipperyComponent> _slipperyQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_slipperyQuery = GetEntityQuery<SlipperyComponent>();
|
||||
|
||||
SubscribeLocalEvent<SlidingComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<SlidingComponent, ComponentShutdown>(OnComponentShutdown);
|
||||
SubscribeLocalEvent<SlidingComponent, StoodEvent>(OnStand);
|
||||
SubscribeLocalEvent<SlidingComponent, StartCollideEvent>(OnStartCollide);
|
||||
SubscribeLocalEvent<SlidingComponent, EndCollideEvent>(OnEndCollide);
|
||||
SubscribeLocalEvent<SlidingComponent, RefreshFrictionModifiersEvent>(OnRefreshFrictionModifiers);
|
||||
SubscribeLocalEvent<SlidingComponent, ThrowerImpulseEvent>(OnThrowerImpulse);
|
||||
SubscribeLocalEvent<SlidingComponent, ShooterImpulseEvent>(ShooterImpulseEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the component is first added, calculate the friction modifier we need.
|
||||
/// Don't do this more than once to avoid mispredicts.
|
||||
/// </summary>
|
||||
private void OnComponentInit(Entity<SlidingComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
if (CalculateSlidingModifier(entity))
|
||||
_speedModifierSystem.RefreshFrictionModifiers(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the component is removed, refresh friction modifiers and set ours to 1 to avoid causing issues.
|
||||
/// </summary>
|
||||
private void OnComponentShutdown(Entity<SlidingComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
entity.Comp.FrictionModifier = 1;
|
||||
_speedModifierSystem.RefreshFrictionModifiers(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -25,28 +59,81 @@ public sealed class SlidingSystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets friction to 0 if colliding with a SuperSlippery Entity.
|
||||
/// Updates friction when we collide with a slippery entity
|
||||
/// </summary>
|
||||
private void OnStartCollide(EntityUid uid, SlidingComponent component, ref StartCollideEvent args)
|
||||
private void OnStartCollide(Entity<SlidingComponent> entity, ref StartCollideEvent args)
|
||||
{
|
||||
if (!TryComp<SlipperyComponent>(args.OtherEntity, out var slippery) || !slippery.SlipData.SuperSlippery)
|
||||
if (!_slipperyQuery.TryComp(args.OtherEntity, out var slippery) || !slippery.AffectsSliding)
|
||||
return;
|
||||
|
||||
component.CollidingEntities.Add(args.OtherEntity);
|
||||
Dirty(uid, component);
|
||||
CalculateSlidingModifier(entity);
|
||||
_speedModifierSystem.RefreshFrictionModifiers(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set friction to normal when ending collision with a SuperSlippery entity.
|
||||
/// Update friction when we stop colliding with a slippery entity
|
||||
/// </summary>
|
||||
private void OnEndCollide(EntityUid uid, SlidingComponent component, ref EndCollideEvent args)
|
||||
private void OnEndCollide(Entity<SlidingComponent> entity, ref EndCollideEvent args)
|
||||
{
|
||||
if (!component.CollidingEntities.Remove(args.OtherEntity))
|
||||
if (!_slipperyQuery.TryComp(args.OtherEntity, out var slippery) || !slippery.AffectsSliding)
|
||||
return;
|
||||
|
||||
if (component.CollidingEntities.Count == 0)
|
||||
RemComp<SlidingComponent>(uid);
|
||||
if (!CalculateSlidingModifier(entity, args.OtherEntity))
|
||||
{
|
||||
RemComp<SlidingComponent>(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
Dirty(uid, component);
|
||||
_speedModifierSystem.RefreshFrictionModifiers(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets contacting slippery entities and averages their friction modifiers.
|
||||
/// </summary>
|
||||
private bool CalculateSlidingModifier(Entity<SlidingComponent, PhysicsComponent?> entity, EntityUid? ignore = null)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp2, false))
|
||||
return false;
|
||||
|
||||
var friction = 0.0f;
|
||||
var count = 0;
|
||||
entity.Comp1.Contacting.Clear();
|
||||
|
||||
_physics.GetContactingEntities((entity, entity.Comp2), entity.Comp1.Contacting);
|
||||
|
||||
foreach (var ent in entity.Comp1.Contacting)
|
||||
{
|
||||
if (ent == ignore || !_slipperyQuery.TryComp(ent, out var slippery) || !slippery.AffectsSliding)
|
||||
continue;
|
||||
|
||||
friction += slippery.SlipData.SlipFriction;
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
entity.Comp1.FrictionModifier = friction / count;
|
||||
Dirty(entity.Owner, entity.Comp1);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnRefreshFrictionModifiers(Entity<SlidingComponent> entity, ref RefreshFrictionModifiersEvent args)
|
||||
{
|
||||
args.ModifyFriction(entity.Comp.FrictionModifier);
|
||||
args.ModifyAcceleration(entity.Comp.FrictionModifier);
|
||||
}
|
||||
|
||||
private void OnThrowerImpulse(Entity<SlidingComponent> entity, ref ThrowerImpulseEvent args)
|
||||
{
|
||||
args.Push = true;
|
||||
}
|
||||
|
||||
private void ShooterImpulseEvent(Entity<SlidingComponent> entity, ref ShooterImpulseEvent args)
|
||||
{
|
||||
args.Push = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,25 @@ namespace Content.Shared.Slippery
|
||||
[Access(Other = AccessPermissions.ReadWriteExecute)]
|
||||
public SoundSpecifier SlipSound = new SoundPathSpecifier("/Audio/Effects/slip.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Should this component's friction factor into sliding friction?
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool AffectsSliding;
|
||||
|
||||
/// <summary>
|
||||
/// How long should this component apply the FrictionStatusComponent?
|
||||
/// Note: This does stack with SlidingComponent since they are two separate Components
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan FrictionStatusTime = TimeSpan.FromSeconds(0.5f);
|
||||
|
||||
/// <summary>
|
||||
/// How much stamina damage should this component do on slip?
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float StaminaDamage = 25f;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the data needed to determine how slippery something is.
|
||||
/// </summary>
|
||||
@@ -34,10 +53,22 @@ namespace Content.Shared.Slippery
|
||||
public sealed partial class SlipperyEffectEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// How many seconds the mob will be paralyzed for.
|
||||
/// How many seconds the mob will be stunned for.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan 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>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float SlipFriction;
|
||||
public float SlipFriction = 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.Network;
|
||||
@@ -23,8 +24,10 @@ namespace Content.Shared.Slippery;
|
||||
public sealed class SlipperySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedStunSystem _stun = default!;
|
||||
[Dependency] private readonly SharedStaminaSystem _stamina = default!;
|
||||
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
@@ -115,27 +118,21 @@ public sealed class SlipperySystem : EntitySystem
|
||||
{
|
||||
_physics.SetLinearVelocity(other, physics.LinearVelocity * component.SlipData.LaunchForwardsMultiplier, body: physics);
|
||||
|
||||
if (component.SlipData.SuperSlippery && requiresContact)
|
||||
{
|
||||
var sliding = EnsureComp<SlidingComponent>(other);
|
||||
sliding.CollidingEntities.Add(uid);
|
||||
// Why the fuck does this assertion stack overflow every once in a while
|
||||
DebugTools.Assert(_physics.GetContactingEntities(other, physics).Contains(uid));
|
||||
}
|
||||
if (component.AffectsSliding && requiresContact)
|
||||
EnsureComp<SlidingComponent>(other);
|
||||
}
|
||||
|
||||
var playSound = !_statusEffects.HasStatusEffect(other, "KnockedDown");
|
||||
|
||||
_stun.TryParalyze(other, component.SlipData.ParalyzeTime, true);
|
||||
|
||||
// Preventing from playing the slip sound when you are already knocked down.
|
||||
if (playSound)
|
||||
// Preventing from playing the slip sound and stunning when you are already knocked down.
|
||||
if (!HasComp<KnockedDownComponent>(other))
|
||||
{
|
||||
_stun.TryStun(other, component.SlipData.StunTime, true);
|
||||
_stamina.TakeStaminaDamage(other, component.StaminaDamage); // Note that this can stamCrit
|
||||
_movementMod.TryFriction(other, component.FrictionStatusTime, true, component.SlipData.SlipFriction, component.SlipData.SlipFriction);
|
||||
_audio.PlayPredicted(component.SlipSound, other, other);
|
||||
}
|
||||
_stun.TryKnockdown(other, component.SlipData.KnockdownTime, true, true);
|
||||
|
||||
_adminLogger.Add(LogType.Slip, LogImpact.Low,
|
||||
$"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(uid):entity}");
|
||||
_adminLogger.Add(LogType.Slip, LogImpact.Low, $"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(uid):entity}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,25 @@ namespace Content.Shared.Standing
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Standing { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Time it takes us to stand up
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan StandTime = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>
|
||||
/// Default Friction modifier for knocked down players.
|
||||
/// Makes them accelerate and deccelerate slower.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float FrictionModifier = 0.4f;
|
||||
|
||||
/// <summary>
|
||||
/// Base modifier to the maximum movement speed of a knocked down mover.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float SpeedModifier = 0.3f;
|
||||
|
||||
/// <summary>
|
||||
/// List of fixtures that had their collision mask changed when the entity was downed.
|
||||
/// Required for re-adding the collision mask.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Rotation;
|
||||
@@ -15,13 +16,16 @@ public sealed class StandingStateSystem : EntitySystem
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
|
||||
// If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited.
|
||||
private const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable;
|
||||
public const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<StandingStateComponent, AttemptMobCollideEvent>(OnMobCollide);
|
||||
SubscribeLocalEvent<StandingStateComponent, AttemptMobTargetCollideEvent>(OnMobTargetCollide);
|
||||
SubscribeLocalEvent<StandingStateComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
|
||||
SubscribeLocalEvent<StandingStateComponent, RefreshFrictionModifiersEvent>(OnRefreshFrictionModifiers);
|
||||
SubscribeLocalEvent<StandingStateComponent, TileFrictionEvent>(OnTileFriction);
|
||||
}
|
||||
|
||||
private void OnMobTargetCollide(Entity<StandingStateComponent> ent, ref AttemptMobTargetCollideEvent args)
|
||||
@@ -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)
|
||||
{
|
||||
if (!Resolve(uid, ref standingState, false))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.StatusEffectNew.Components;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
@@ -9,6 +11,9 @@ public sealed partial class StatusEffectsSystem
|
||||
{
|
||||
SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerAttachedEvent>(RelayStatusEffectEvent);
|
||||
SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerDetachedEvent>(RelayStatusEffectEvent);
|
||||
|
||||
SubscribeLocalEvent<StatusEffectContainerComponent, RefreshFrictionModifiersEvent>(RefRelayStatusEffectEvent);
|
||||
SubscribeLocalEvent<StatusEffectContainerComponent, TileFrictionEvent>(RefRelayStatusEffectEvent);
|
||||
}
|
||||
|
||||
private void RefRelayStatusEffectEvent<T>(EntityUid uid, StatusEffectContainerComponent component, ref T args) where T : struct
|
||||
|
||||
@@ -1,18 +1,46 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Content.Shared.DoAfter;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Stunnable;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStunSystem))]
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas:true), AutoGenerateComponentPause, Access(typeof(SharedStunSystem))]
|
||||
public sealed partial class KnockedDownComponent : Component
|
||||
{
|
||||
[DataField("helpInterval"), AutoNetworkedField]
|
||||
public float HelpInterval = 1f;
|
||||
/// <summary>
|
||||
/// Game time that we can stand up.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
|
||||
public TimeSpan NextUpdate;
|
||||
|
||||
[DataField("helpAttemptSound")]
|
||||
public SoundSpecifier StunAttemptSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
|
||||
/// <summary>
|
||||
/// Should we try to stand up?
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool AutoStand = true;
|
||||
|
||||
[ViewVariables, AutoNetworkedField]
|
||||
public float HelpTimer = 0f;
|
||||
/// <summary>
|
||||
/// The Standing Up DoAfter.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public ushort? DoAfterId;
|
||||
|
||||
/// <summary>
|
||||
/// Friction modifier for knocked down players.
|
||||
/// Makes them accelerate and deccelerate slower.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float FrictionModifier = 1f; // Should add a friction modifier to slipping to compensate for this
|
||||
|
||||
/// <summary>
|
||||
/// Modifier to the maximum movement speed of a knocked down mover.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float SpeedModifier = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// How long does it take us to get up?
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan GetUpDoAfter = TimeSpan.FromSeconds(1);
|
||||
}
|
||||
|
||||
538
Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs
Normal file
538
Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs
Normal 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
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
@@ -17,9 +18,7 @@ using Content.Shared.StatusEffect;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Stunnable;
|
||||
@@ -27,40 +26,28 @@ namespace Content.Shared.Stunnable;
|
||||
public abstract partial class SharedStunSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly ActionBlockerSystem Blocker = default!;
|
||||
[Dependency] protected readonly AlertsSystem Alerts = default!;
|
||||
[Dependency] protected readonly IGameTiming GameTiming = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
|
||||
[Dependency] private readonly StandingStateSystem _standingState = default!;
|
||||
[Dependency] protected readonly SharedDoAfterSystem DoAfter = default!;
|
||||
[Dependency] protected readonly SharedStaminaSystem Stamina = default!;
|
||||
[Dependency] private readonly StatusEffectsSystem _statusEffect = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Friction modifier for knocked down players.
|
||||
/// Doesn't make them faster but makes them slow down... slower.
|
||||
/// </summary>
|
||||
public const float KnockDownModifier = 0.2f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<KnockedDownComponent, ComponentInit>(OnKnockInit);
|
||||
SubscribeLocalEvent<KnockedDownComponent, ComponentShutdown>(OnKnockShutdown);
|
||||
SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandAttempt);
|
||||
|
||||
SubscribeLocalEvent<SlowedDownComponent, ComponentInit>(OnSlowInit);
|
||||
SubscribeLocalEvent<SlowedDownComponent, ComponentShutdown>(OnSlowRemove);
|
||||
SubscribeLocalEvent<SlowedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
|
||||
|
||||
SubscribeLocalEvent<StunnedComponent, ComponentStartup>(UpdateCanMove);
|
||||
SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(OnStunShutdown);
|
||||
|
||||
SubscribeLocalEvent<StunOnContactComponent, StartCollideEvent>(OnStunOnContactCollide);
|
||||
|
||||
// helping people up if they're knocked down
|
||||
SubscribeLocalEvent<KnockedDownComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<SlowedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
|
||||
|
||||
SubscribeLocalEvent<KnockedDownComponent, TileFrictionEvent>(OnKnockedTileFriction);
|
||||
|
||||
// Attempt event subscriptions.
|
||||
SubscribeLocalEvent<StunnedComponent, ChangeDirectionAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<StunnedComponent, UpdateCanMoveEvent>(OnMoveAttempt);
|
||||
@@ -74,7 +61,7 @@ public abstract partial class SharedStunSystem : EntitySystem
|
||||
SubscribeLocalEvent<StunnedComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
|
||||
SubscribeLocalEvent<MobStateComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
|
||||
// Stun Appearance Data
|
||||
InitializeKnockdown();
|
||||
InitializeAppearance();
|
||||
}
|
||||
|
||||
@@ -136,23 +123,7 @@ public abstract partial class SharedStunSystem : EntitySystem
|
||||
return;
|
||||
|
||||
TryStun(args.OtherEntity, ent.Comp.Duration, true, status);
|
||||
TryKnockdown(args.OtherEntity, ent.Comp.Duration, true, status);
|
||||
}
|
||||
|
||||
private void OnKnockInit(EntityUid uid, KnockedDownComponent component, ComponentInit args)
|
||||
{
|
||||
_standingState.Down(uid);
|
||||
}
|
||||
|
||||
private void OnKnockShutdown(EntityUid uid, KnockedDownComponent component, ComponentShutdown args)
|
||||
{
|
||||
_standingState.Stand(uid);
|
||||
}
|
||||
|
||||
private void OnStandAttempt(EntityUid uid, KnockedDownComponent component, StandAttemptEvent args)
|
||||
{
|
||||
if (component.LifeStage <= ComponentLifeStage.Running)
|
||||
args.Cancel();
|
||||
TryKnockdown(args.OtherEntity, ent.Comp.Duration, ent.Comp.Refresh, ent.Comp.AutoStand);
|
||||
}
|
||||
|
||||
private void OnSlowInit(EntityUid uid, SlowedDownComponent component, ComponentInit args)
|
||||
@@ -167,18 +138,12 @@ public abstract partial class SharedStunSystem : EntitySystem
|
||||
_movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
|
||||
}
|
||||
|
||||
private void OnRefreshMovespeed(EntityUid uid, SlowedDownComponent component, RefreshMovementSpeedModifiersEvent args)
|
||||
{
|
||||
args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
|
||||
}
|
||||
|
||||
// TODO STUN: Make events for different things. (Getting modifiers, attempt events, informative events...)
|
||||
|
||||
/// <summary>
|
||||
/// Stuns the entity, disallowing it from doing many interactions temporarily.
|
||||
/// </summary>
|
||||
public bool TryStun(EntityUid uid, TimeSpan time, bool refresh,
|
||||
StatusEffectsComponent? status = null)
|
||||
public bool TryStun(EntityUid uid, TimeSpan time, bool refresh, StatusEffectsComponent? status = null)
|
||||
{
|
||||
if (time <= TimeSpan.Zero)
|
||||
return false;
|
||||
@@ -199,20 +164,48 @@ public abstract partial class SharedStunSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Knocks down the entity, making it fall to the ground.
|
||||
/// </summary>
|
||||
public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh,
|
||||
StatusEffectsComponent? status = null)
|
||||
public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh, bool autoStand = true, bool drop = true)
|
||||
{
|
||||
if (time <= TimeSpan.Zero)
|
||||
return false;
|
||||
|
||||
if (!Resolve(uid, ref status, false))
|
||||
// Can't fall down if you can't actually be downed.
|
||||
if (!HasComp<StandingStateComponent>(uid))
|
||||
return false;
|
||||
|
||||
if (!_statusEffect.TryAddStatusEffect<KnockedDownComponent>(uid, "KnockedDown", time, refresh))
|
||||
var evAttempt = new KnockDownAttemptEvent(autoStand, drop);
|
||||
RaiseLocalEvent(uid, ref evAttempt);
|
||||
|
||||
if (evAttempt.Cancelled)
|
||||
return false;
|
||||
|
||||
var ev = new KnockedDownEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
// Initialize our component with the relevant data we need if we don't have it
|
||||
if (EnsureComp<KnockedDownComponent>(uid, out var component))
|
||||
{
|
||||
RefreshKnockedMovement((uid, component));
|
||||
CancelKnockdownDoAfter((uid, component));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only drop items the first time we want to fall...
|
||||
if (drop)
|
||||
{
|
||||
var ev = new DropHandItemsEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
|
||||
// Only update Autostand value if it's our first time being knocked down...
|
||||
SetAutoStand((uid, component), evAttempt.AutoStand);
|
||||
}
|
||||
|
||||
var knockedEv = new KnockedDownEvent(time);
|
||||
RaiseLocalEvent(uid, ref knockedEv);
|
||||
|
||||
UpdateKnockdownTime((uid, component), knockedEv.Time, refresh);
|
||||
|
||||
Alerts.ShowAlert(uid, KnockdownAlert, null, (GameTiming.CurTime, component.NextUpdate));
|
||||
|
||||
_adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} knocked down for {time.Seconds} seconds");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -226,14 +219,14 @@ public abstract partial class SharedStunSystem : EntitySystem
|
||||
if (!Resolve(uid, ref status, false))
|
||||
return false;
|
||||
|
||||
return TryKnockdown(uid, time, refresh, status) && TryStun(uid, time, refresh, status);
|
||||
return TryKnockdown(uid, time, refresh) && TryStun(uid, time, refresh, status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slows down the mob's walking/running speed temporarily
|
||||
/// </summary>
|
||||
public bool TrySlowdown(EntityUid uid, TimeSpan time, bool refresh,
|
||||
float walkSpeedMultiplier = 1f, float runSpeedMultiplier = 1f,
|
||||
float walkSpeedMod = 1f, float sprintSpeedMod = 1f,
|
||||
StatusEffectsComponent? status = null)
|
||||
{
|
||||
if (!Resolve(uid, ref status, false))
|
||||
@@ -246,11 +239,11 @@ public abstract partial class SharedStunSystem : EntitySystem
|
||||
{
|
||||
var slowed = Comp<SlowedDownComponent>(uid);
|
||||
// Doesn't make much sense to have the "TrySlowdown" method speed up entities now does it?
|
||||
walkSpeedMultiplier = Math.Clamp(walkSpeedMultiplier, 0f, 1f);
|
||||
runSpeedMultiplier = Math.Clamp(runSpeedMultiplier, 0f, 1f);
|
||||
walkSpeedMod = Math.Clamp(walkSpeedMod, 0f, 1f);
|
||||
sprintSpeedMod = Math.Clamp(sprintSpeedMod, 0f, 1f);
|
||||
|
||||
slowed.WalkSpeedModifier *= walkSpeedMultiplier;
|
||||
slowed.SprintSpeedModifier *= runSpeedMultiplier;
|
||||
slowed.WalkSpeedModifier *= walkSpeedMod;
|
||||
slowed.SprintSpeedModifier *= sprintSpeedMod;
|
||||
|
||||
_movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
|
||||
return true;
|
||||
@@ -310,29 +303,14 @@ public abstract partial class SharedStunSystem : EntitySystem
|
||||
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)
|
||||
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;
|
||||
args.ModifySpeed(comp.WalkSpeedModifier, comp.SprintSpeedModifier);
|
||||
}
|
||||
|
||||
private void OnKnockedTileFriction(EntityUid uid, KnockedDownComponent component, ref TileFrictionEvent args)
|
||||
{
|
||||
args.Modifier *= KnockDownModifier;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Attempt Event Handling
|
||||
|
||||
@@ -365,15 +343,3 @@ public abstract partial class SharedStunSystem : EntitySystem
|
||||
|
||||
#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;
|
||||
|
||||
@@ -18,6 +18,18 @@ public sealed partial class StunOnContactComponent : Component
|
||||
[DataField]
|
||||
public TimeSpan Duration = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Should the stun applied refresh?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Refresh = true;
|
||||
|
||||
/// <summary>
|
||||
/// Should the stunned entity try to stand up when knockdown ends?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool AutoStand = true;
|
||||
|
||||
[DataField]
|
||||
public EntityWhitelist Blacklist = new();
|
||||
}
|
||||
|
||||
89
Content.Shared/Stunnable/StunnableEvents.cs
Normal file
89
Content.Shared/Stunnable/StunnableEvents.cs
Normal 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;
|
||||
|
||||
@@ -25,4 +25,13 @@
|
||||
/// Raised when we try to pushback an entity from throwing
|
||||
/// </summary>
|
||||
public sealed class ThrowPushbackAttemptEvent : CancellableEntityEventArgs {}
|
||||
|
||||
/// <summary>
|
||||
/// Raised on an entity that is being pushed from a thrown entity
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct ThrowerImpulseEvent()
|
||||
{
|
||||
public bool Push;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,17 +222,21 @@ public sealed class ThrowingSystem : EntitySystem
|
||||
_recoil.KickCamera(user.Value, -direction * 0.04f);
|
||||
|
||||
// Give thrower an impulse in the other direction
|
||||
if (pushbackRatio != 0.0f &&
|
||||
physics.Mass > 0f &&
|
||||
TryComp(user.Value, out PhysicsComponent? userPhysics) &&
|
||||
_gravity.IsWeightless(user.Value, userPhysics))
|
||||
{
|
||||
var msg = new ThrowPushbackAttemptEvent();
|
||||
RaiseLocalEvent(uid, msg);
|
||||
const float massLimit = 5f;
|
||||
if (pushbackRatio == 0.0f ||
|
||||
physics.Mass == 0f ||
|
||||
!TryComp(user.Value, out PhysicsComponent? userPhysics))
|
||||
return;
|
||||
var msg = new ThrowPushbackAttemptEvent();
|
||||
RaiseLocalEvent(uid, msg);
|
||||
|
||||
if (!msg.Cancelled)
|
||||
_physics.ApplyLinearImpulse(user.Value, -impulseVector / physics.Mass * pushbackRatio * MathF.Min(massLimit, physics.Mass), body: userPhysics);
|
||||
}
|
||||
if (msg.Cancelled)
|
||||
return;
|
||||
|
||||
var pushEv = new ThrowerImpulseEvent();
|
||||
RaiseLocalEvent(user.Value, ref pushEv);
|
||||
const float massLimit = 5f;
|
||||
|
||||
if (pushEv.Push || _gravity.IsWeightless(user.Value))
|
||||
_physics.ApplyLinearImpulse(user.Value, -impulseVector / physics.Mass * pushbackRatio * MathF.Min(massLimit, physics.Mass), body: userPhysics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,11 +384,14 @@ public abstract partial class SharedGunSystem : EntitySystem
|
||||
var shotEv = new GunShotEvent(user, ev.Ammo);
|
||||
RaiseLocalEvent(gunUid, ref shotEv);
|
||||
|
||||
if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics))
|
||||
{
|
||||
if (_gravity.IsWeightless(user, userPhysics))
|
||||
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
|
||||
}
|
||||
if (!userImpulse || !TryComp<PhysicsComponent>(user, out var userPhysics))
|
||||
return;
|
||||
|
||||
var shooterEv = new ShooterImpulseEvent();
|
||||
RaiseLocalEvent(user, ref shooterEv);
|
||||
|
||||
if (shooterEv.Push || _gravity.IsWeightless(user, userPhysics))
|
||||
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
|
||||
}
|
||||
|
||||
public void Shoot(
|
||||
@@ -629,6 +632,16 @@ public record struct AttemptShootEvent(EntityUid User, string? Message, bool Can
|
||||
[ByRefEvent]
|
||||
public record struct GunShotEvent(EntityUid User, List<(EntityUid? Uid, IShootable Shootable)> Ammo);
|
||||
|
||||
/// <summary>
|
||||
/// Raised on an entity after firing a gun to see if any components or systems would allow this entity to be pushed
|
||||
/// by the gun they're firing. If true, GunSystem will create an impulse on our entity.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct ShooterImpulseEvent()
|
||||
{
|
||||
public bool Push;
|
||||
};
|
||||
|
||||
public enum EffectLayers : byte
|
||||
{
|
||||
Unshaded,
|
||||
|
||||
@@ -57,6 +57,7 @@ admin-smite-ghostkick-name = Ghost Kick
|
||||
admin-smite-nyanify-name = Cat Ears
|
||||
admin-smite-kill-sign-name = Kill Sign
|
||||
admin-smite-omni-accent-name = Omni-Accent
|
||||
admin-smite-crawler-name = Crawler
|
||||
|
||||
## Smite descriptions
|
||||
|
||||
@@ -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-super-slip-description = Slips them really, really hard.
|
||||
admin-smite-omni-accent-description = Makes the target speak with almost every accent available.
|
||||
admin-smite-crawler-description = Makes the target fall down and be unable to stand up. Remove their hands too for added effect!
|
||||
|
||||
## Tricks descriptions
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ alerts-walking-desc = You are walking, moving at a slow pace.
|
||||
alerts-stunned-name = [color=yellow]Stunned[/color]
|
||||
alerts-stunned-desc = You're [color=yellow]stunned[/color]! Something is impairing your ability to move or interact with objects.
|
||||
|
||||
alerts-knockdown-name = [color=yellow]Knocked Down[/color]
|
||||
alerts-knockdown-desc = You're [color=yellow]Knocked Down[/color]! Something has slipped or pushed you over, encumbering your movement.
|
||||
|
||||
alerts-handcuffed-name = [color=yellow]Handcuffed[/color]
|
||||
alerts-handcuffed-desc = You're [color=yellow]handcuffed[/color] and can't use your hands. If anyone drags you, you won't be able to resist.
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ ui-options-function-move-left = Move Left
|
||||
ui-options-function-move-down = Move Down
|
||||
ui-options-function-move-right = Move Right
|
||||
ui-options-function-walk = Walk
|
||||
ui-options-function-toggle-knockdown = Toggle Crawling
|
||||
|
||||
ui-options-function-camera-rotate-left = Rotate left
|
||||
ui-options-function-camera-rotate-right = Rotate right
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
stunnable-component-disarm-success-others = {CAPITALIZE(THE($source))} pushes {THE($target)}!
|
||||
stunnable-component-disarm-success = You push {THE($target)}!
|
||||
knockdown-component-pushup-failure = You're too exhausted to push yourself up!
|
||||
knockdown-component-pushup-success = With a burst of energy you push yourself up!
|
||||
knockdown-component-stand-no-room = You try to push yourself to stand up but there's not enough room!
|
||||
worm-component-stand-attempt = You try to stand up but you cannot!
|
||||
|
||||
@@ -146,6 +146,15 @@
|
||||
name: alerts-stunned-name
|
||||
description: alerts-stunned-desc
|
||||
|
||||
- type: alert
|
||||
id: Knockdown
|
||||
clickEvent: !type:KnockedDownAlertEvent
|
||||
icons:
|
||||
- sprite: /Textures/Interface/Alerts/stunnable.rsi
|
||||
state: knocked-down
|
||||
name: alerts-knockdown-name
|
||||
description: alerts-knockdown-desc
|
||||
|
||||
- type: alert
|
||||
id: Handcuffed
|
||||
clickEvent: !type:RemoveCuffsAlertEvent
|
||||
|
||||
@@ -159,6 +159,8 @@
|
||||
components:
|
||||
- type: Clickable
|
||||
- type: Slippery
|
||||
staminaDamage: 0
|
||||
frictionStatusTime: 0 # Don't apply friction twice
|
||||
- type: Transform
|
||||
noRot: true
|
||||
anchored: true
|
||||
|
||||
@@ -144,7 +144,6 @@
|
||||
- type: StatusEffects
|
||||
allowed:
|
||||
- Stun
|
||||
- KnockedDown
|
||||
- SlowedDown
|
||||
- Flashed
|
||||
- type: TypingIndicator
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
- type: StatusEffects
|
||||
allowed:
|
||||
- Stun
|
||||
- KnockedDown
|
||||
- SlowedDown
|
||||
- Stutter
|
||||
- Electrocution
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
- type: StatusEffects
|
||||
allowed:
|
||||
- Stun
|
||||
- KnockedDown
|
||||
- SlowedDown
|
||||
- Stutter
|
||||
- Electrocution
|
||||
|
||||
@@ -94,7 +94,6 @@
|
||||
- type: StatusEffects
|
||||
allowed:
|
||||
- Stun
|
||||
- KnockedDown
|
||||
- SlowedDown
|
||||
- Stutter
|
||||
- Electrocution
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
- type: StatusEffects
|
||||
allowed:
|
||||
- Stun
|
||||
- KnockedDown
|
||||
- Friction
|
||||
- SlowedDown
|
||||
- Stutter
|
||||
- Electrocution
|
||||
|
||||
@@ -81,6 +81,26 @@
|
||||
- type: TrashOnSolutionEmpty
|
||||
solution: drink
|
||||
|
||||
- type: entity
|
||||
parent: BluespaceBeaker
|
||||
id: BottomlessLube
|
||||
name: bottomless lube beaker
|
||||
suffix: DEBUG
|
||||
description: This anomalous beaker infinitely produces space lube and as such is to be closely guarded such that it doesn't fall in the wrong hands.
|
||||
components:
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
beaker:
|
||||
maxVol: 1000
|
||||
reagents:
|
||||
- ReagentId: SpaceLube
|
||||
Quantity: 1000
|
||||
- type: SolutionRegeneration
|
||||
solution: beaker
|
||||
generated:
|
||||
reagents:
|
||||
- ReagentId: SpaceLube
|
||||
Quantity: 200
|
||||
|
||||
# Mopwata
|
||||
- type: weightedRandomFillSolution
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
Quantity: 5
|
||||
- type: Slippery
|
||||
slipData:
|
||||
paralyzeTime: 3
|
||||
knockdownTime: 3
|
||||
launchForwardsMultiplier: 3
|
||||
- type: StepTrigger
|
||||
intersectRatio: 0.2
|
||||
|
||||
@@ -872,8 +872,10 @@
|
||||
- type: UseDelay
|
||||
delay: 0.8
|
||||
- type: Slippery
|
||||
staminaDamage: 0
|
||||
slipData:
|
||||
paralyzeTime: 0
|
||||
stunTime: 0
|
||||
knockdownTime: 0
|
||||
launchForwardsMultiplier: 0
|
||||
slipSound:
|
||||
collection: Parp
|
||||
|
||||
@@ -127,8 +127,9 @@
|
||||
- type: SolutionContainerVisuals
|
||||
fillBaseName: syndie-
|
||||
- type: Slippery
|
||||
staminaDamage: 50
|
||||
slipData:
|
||||
paralyzeTime: 3
|
||||
knockdownTime: 3
|
||||
launchForwardsMultiplier: 3
|
||||
- type: Item
|
||||
heldPrefix: syndie
|
||||
@@ -155,9 +156,6 @@
|
||||
layers:
|
||||
- state: syndie-soaplet
|
||||
- type: Slippery
|
||||
slipData:
|
||||
paralyzeTime: 1.5 # these things are tiny
|
||||
launchForwardsMultiplier: 1.5
|
||||
- type: StepTrigger
|
||||
intersectRatio: 0.04
|
||||
- type: Item
|
||||
@@ -222,8 +220,9 @@
|
||||
- type: SolutionContainerVisuals
|
||||
fillBaseName: omega-
|
||||
- type: Slippery
|
||||
staminaDamage: 50
|
||||
slipData:
|
||||
paralyzeTime: 5.0
|
||||
knockdownTime: 5.0
|
||||
launchForwardsMultiplier: 3.0
|
||||
- type: Item
|
||||
heldPrefix: omega
|
||||
|
||||
@@ -547,7 +547,7 @@
|
||||
name: taser
|
||||
parent: [BaseWeaponBatterySmall, BaseSecurityContraband]
|
||||
id: WeaponTaser
|
||||
description: A low-capacity, energy-based stun gun used by security teams to subdue targets at range.
|
||||
description: A low-capacity, energy-based stun gun used by security teams to subdue targets at close range.
|
||||
components:
|
||||
- type: Tag
|
||||
tags:
|
||||
@@ -579,6 +579,23 @@
|
||||
zeroVisible: true
|
||||
- type: Appearance
|
||||
|
||||
- type: entity
|
||||
name: elite taser
|
||||
parent: [ BaseCentcommContraband, WeaponTaser ]
|
||||
id: WeaponTaserSuper
|
||||
suffix: ADMEME
|
||||
description: A low-capacity, energy-based stun gun used by elite security teams to disable even the toughest of targets.
|
||||
components:
|
||||
- type: Gun
|
||||
fireRate: 0.5
|
||||
soundGunshot:
|
||||
path: /Audio/Effects/tesla_collapse.ogg # The wrath of god...
|
||||
params:
|
||||
volume: -6
|
||||
- type: ProjectileBatteryAmmoProvider
|
||||
proto: BulletTaserSuper
|
||||
fireCost: 200
|
||||
|
||||
- type: entity
|
||||
name: antique laser pistol
|
||||
parent: [BaseWeaponBatterySmall, BaseGrandTheftContraband]
|
||||
|
||||
@@ -216,13 +216,46 @@
|
||||
- type: Projectile
|
||||
damage:
|
||||
types:
|
||||
Heat: 5
|
||||
Shock: 1
|
||||
soundHit:
|
||||
path: "/Audio/Weapons/Guns/Hits/taser_hit.ogg"
|
||||
forceSound: true
|
||||
- type: TimedDespawn
|
||||
lifetime: 0.170 # Very short range
|
||||
- type: StunOnCollide
|
||||
stunAmount: 0
|
||||
knockdownAmount: 2.5 # Enough to subdue and follow up with a stun batong
|
||||
slowdownAmount: 2.5
|
||||
walkSpeedModifier: 0.5
|
||||
sprintSpeedModifier: 0.5
|
||||
|
||||
- type: entity
|
||||
parent: BulletTaser
|
||||
id: BulletTaserSuper
|
||||
categories: [ HideSpawnMenu ]
|
||||
name: taser bolt
|
||||
description: If you can see this, you've probably been stun-meta'd
|
||||
components:
|
||||
- type: Sprite
|
||||
noRot: true
|
||||
sprite: Structures/Power/Generation/Tesla/energy_miniball.rsi
|
||||
color: "#ffff33"
|
||||
layers:
|
||||
- state: tesla_projectile
|
||||
shader: unshaded
|
||||
- type: PointLight
|
||||
enabled: true
|
||||
color: "#ffff33"
|
||||
radius: 2.0
|
||||
energy: 7.01
|
||||
- type: TimedDespawn
|
||||
lifetime: 1.0 # Not so short range
|
||||
- type: StunOnCollide
|
||||
stunAmount: 5
|
||||
knockdownAmount: 5
|
||||
knockdownAmount: 10
|
||||
slowdownAmount: 10
|
||||
walkSpeedModifier: 0.5
|
||||
sprintSpeedModifier: 0.5
|
||||
|
||||
- type: entity
|
||||
name : disabler bolt
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
components:
|
||||
- type: DrowsinessStatusEffect
|
||||
|
||||
# Makes you more slippery, or perhaps less slippery.
|
||||
- type: entity
|
||||
parent: MobStatusEffectBase
|
||||
id: StatusEffectFriction
|
||||
name: friction
|
||||
components:
|
||||
- type: FrictionStatusEffect
|
||||
|
||||
# Adds drugs overlay
|
||||
- type: entity
|
||||
parent: MobStatusEffectBase
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
meltingPoint: 18.2
|
||||
tileReactions:
|
||||
- !type:SpillTileReaction
|
||||
friction: 0.0
|
||||
friction: 0.05
|
||||
|
||||
- type: reagent
|
||||
id: SpaceGlue
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
id: Stun
|
||||
alert: Stun
|
||||
|
||||
- type: statusEffect
|
||||
id: KnockedDown
|
||||
alert: Stun
|
||||
|
||||
- type: statusEffect
|
||||
id: SlowedDown
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
14
Resources/Textures/Interface/Alerts/stunnable.rsi/meta.json
Normal file
14
Resources/Textures/Interface/Alerts/stunnable.rsi/meta.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -190,7 +190,7 @@ binds:
|
||||
mod1: Alt
|
||||
- function: OpenCharacterMenu
|
||||
type: State
|
||||
key: C
|
||||
key: U
|
||||
- function: OpenEmotesMenu
|
||||
type: State
|
||||
key: Y
|
||||
@@ -525,6 +525,9 @@ binds:
|
||||
- function: Arcade3
|
||||
type: State
|
||||
key: Z
|
||||
- function: ToggleKnockdown
|
||||
type: State
|
||||
key: C
|
||||
- function: OpenAbilitiesMenu
|
||||
type: State
|
||||
key: K
|
||||
|
||||
Reference in New Issue
Block a user