mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-17 13:37:47 +03:00
# Description Implements the softcrit functionality. Similiar to critical state but spessmen will be able to communicate and crawl around, but not pick up items. Also supports configuring what is and isn't allowed in different MobStates (per mob prototype): you can enable picking up items while in softcrit so people can pick up their lasgun and continue shooting after taking a 40x46mm to their ass cheeks from the guest nukies while being dragged to safety.  <details> <summary><h1>Technical details</h1></summary> New prototype type: "mobStateParams" (`MobStateParametersPrototype`) Used to specify what can and can't be done when in a certain mobstate. Of note that they are not actually bound to any `MobState` by themselves. To assign a params prototype to a mobstate, use `InitMobStateParams` in `MobStateComponent`. It has to be a prototype because if I just did something akin to `Dictionary<MobState, Dictionary<string, bool>>`, you'd have to check the parent and copy every flag besides the one you wish to modify. That is, if I understand how the prototype system works correctly, which I frankly doubt. <!-- Working on softcrit made me hate prototypes. --> MobStateComponent now has: - `Dictionary<string, string> InitMobStateParams`, for storing "mobstate - parameter prototype" pairs. `<string, string>` because it has to be editable via mob prototypes. Named "mobStateParams" for mob prototypes. - `public Dictionary<MobState, MobStateParametersPrototype> MobStateParams` for actually storing the params for each state - `public Dictionary<MobState, MobStateParametersOverride> MobStateParamsOverrides` for storing overrides. `MobStateParametersOverride` is a struct which mirrors all `MobStateParametersPrototype`'s fields, except they're all nullable. This is meant for code which wants to temporarily override some setting, like a spell which allows dead people to talk. This is not the best solution, but it should do at first. A better option would be tracking each change separately, instead of hoping different systems overriding the same flag will play nicely with eachother. - a shitton of getter methods TraitModifyMobState now has: - `public Dictionary<string, string> Params` to specify a new prototype to use. - Important note: All values of `MobStateParametersPrototype` are nullable, which is a hack to support `TraitModifyMobState`. This trait takes one `MobStateParametersPrototype` per mobstate and applies all of its non-null values. This way, a params prototype can be created which will only have `pointing: true` and the trait can apply it (e.g. to critstate, so we can spam pointing while dying like it's a game of turbo dota) - The above is why that wall of getters exists: They check the relevant override struct, then the relevant prototype. If both are null, they default to false (0f for floats.) The only exception is OxyDamageOverlay, because it's used both for oxy damage overlay (if null) and as a vision-limiting black void in crit.. MobStateSystem now has: - a bunch of new "IsSomething"/"CanDoSomething" methods to check the various flags, alongside rewritten old ones. -  lookin ahh predicate factory </details> --- # TODO done: - [x] Make proper use of `MobStateSystem.IsIncapacitated()`. done: some checks were changed, some left as they did what was (more or less) intended. <details>Previous `IsIncapacitated()` implementation simply checked if person was in crit or dead. Now there is a `IsIncapacitated` flag in the parameters, but it's heavily underutilized. I may need some help on this one, since I don't know where would be a good place to check for it and I absolutely will not just scour the entire build in search for them. </details> - [x] Separate force-dropping items from being downed done: dropItemsOnEntering bool field. If true, will drop items upon entering linked mobstate. - [x] Don't drop items if `ForceDown` is true but `PickingUp` is also true. done: dropItemsOnEntering bool field. If true, will drop items upon entering linked mobstate. - [x] Actually check what are "conscious attempts" are used for done: whether or not mob is conscious. Renamed the bool field accordingly. - [x] Look into adding a way to make people choke "slowly" in softcrit as opposed to choking at "regular speed" in crit. Make that into a param option? Make that into a float so the speed can be finetuned? done: `BreathingMultiplier` float field added. <details> 1f is regular breathing, 0.25 is "quarter-breathing". Air taken is multiplied by `BreathingMultiplier` and suffocation damage taken (that is dealt by RespiratorSystem, not all oxy damage) is multiplied by `1-BreathingMultiplier`. </details> - [x] make sure the serializer actually does its job done: it doesn't. Removed. - [x] Make an option to prohibit using radio headsets while in softcrit done: Requires Incapacitated parameter to be false to be able to use headset radio. - [x] Make sure it at least compiles not done: - [ ] probably move some other stuff to Params if it makes sense. Same thing as with `IsIncapacitated` though: I kinda don't want to, at least for now. --- <details><summary><h1>No media</h1></summary> <p> :p </p> </details> --- # Changelog 🆑 - add: Soft critical state. Crawl to safety, or to your doom - whatever is closer. --------- Signed-off-by: RedFoxIV <38788538+RedFoxIV@users.noreply.github.com> Co-authored-by: VMSolidus <evilexecutive@gmail.com> (cherry picked from commit 9a357c1774f1a783844a07b5414f504ca574d84c)
366 lines
16 KiB
C#
366 lines
16 KiB
C#
using System.Numerics;
|
|
using System.Threading;
|
|
using Content.Server.DoAfter;
|
|
using Content.Server.Resist;
|
|
using Content.Server.Popups;
|
|
using Content.Server.Inventory;
|
|
using Content.Server.Nyanotrasen.Item.PseudoItem;
|
|
using Content.Shared.Mobs;
|
|
using Content.Shared.DoAfter;
|
|
using Content.Shared.Buckle.Components;
|
|
using Content.Shared.Hands.Components;
|
|
using Content.Shared.Hands;
|
|
using Content.Shared.Stunnable;
|
|
using Content.Shared.Interaction.Events;
|
|
using Content.Shared.Verbs;
|
|
using Content.Shared.Climbing.Events;
|
|
using Content.Shared.Carrying;
|
|
using Content.Shared.Contests;
|
|
using Content.Shared.Movement.Events;
|
|
using Content.Shared.Movement.Systems;
|
|
using Content.Shared.Standing;
|
|
using Content.Shared.ActionBlocker;
|
|
using Content.Shared.Inventory.VirtualItem;
|
|
using Content.Shared.Item;
|
|
using Content.Shared.Throwing;
|
|
using Content.Shared.Movement.Pulling.Components;
|
|
using Content.Shared.Movement.Pulling.Events;
|
|
using Content.Shared.Movement.Pulling.Systems;
|
|
using Content.Shared.Mobs.Systems;
|
|
using Content.Shared.Nyanotrasen.Item.PseudoItem;
|
|
using Content.Shared.Storage;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Physics.Components;
|
|
using Robust.Server.GameObjects;
|
|
|
|
namespace Content.Server.Carrying
|
|
{
|
|
public sealed class CarryingSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly VirtualItemSystem _virtualItemSystem = default!;
|
|
[Dependency] private readonly CarryingSlowdownSystem _slowdown = default!;
|
|
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
|
[Dependency] private readonly StandingStateSystem _standingState = default!;
|
|
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
|
[Dependency] private readonly PullingSystem _pullingSystem = default!;
|
|
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
|
[Dependency] private readonly EscapeInventorySystem _escapeInventorySystem = default!;
|
|
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
|
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
|
|
[Dependency] private readonly PseudoItemSystem _pseudoItem = default!;
|
|
[Dependency] private readonly ContestsSystem _contests = default!;
|
|
[Dependency] private readonly TransformSystem _transform = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
SubscribeLocalEvent<CarriableComponent, GetVerbsEvent<AlternativeVerb>>(AddCarryVerb);
|
|
SubscribeLocalEvent<CarryingComponent, GetVerbsEvent<InnateVerb>>(AddInsertCarriedVerb);
|
|
SubscribeLocalEvent<CarryingComponent, VirtualItemDeletedEvent>(OnVirtualItemDeleted);
|
|
SubscribeLocalEvent<CarryingComponent, BeforeThrowEvent>(OnThrow);
|
|
SubscribeLocalEvent<CarryingComponent, EntParentChangedMessage>(OnParentChanged);
|
|
SubscribeLocalEvent<CarryingComponent, MobStateChangedEvent>(OnMobStateChanged);
|
|
SubscribeLocalEvent<BeingCarriedComponent, InteractionAttemptEvent>(OnInteractionAttempt);
|
|
SubscribeLocalEvent<BeingCarriedComponent, MoveInputEvent>(OnMoveInput);
|
|
SubscribeLocalEvent<BeingCarriedComponent, UpdateCanMoveEvent>(OnMoveAttempt);
|
|
SubscribeLocalEvent<BeingCarriedComponent, StandAttemptEvent>(OnStandAttempt);
|
|
SubscribeLocalEvent<BeingCarriedComponent, GettingInteractedWithAttemptEvent>(OnInteractedWith);
|
|
SubscribeLocalEvent<BeingCarriedComponent, PullAttemptEvent>(OnPullAttempt);
|
|
SubscribeLocalEvent<BeingCarriedComponent, StartClimbEvent>(OnStartClimb);
|
|
SubscribeLocalEvent<BeingCarriedComponent, BuckledEvent>(OnBuckled);
|
|
SubscribeLocalEvent<CarriableComponent, CarryDoAfterEvent>(OnDoAfter);
|
|
}
|
|
|
|
private void AddCarryVerb(EntityUid uid, CarriableComponent component, GetVerbsEvent<AlternativeVerb> args)
|
|
{
|
|
if (!args.CanInteract || !args.CanAccess || !_mobStateSystem.IsAlive(args.User)
|
|
|| !CanCarry(args.User, uid, component)
|
|
|| HasComp<CarryingComponent>(args.User)
|
|
|| HasComp<BeingCarriedComponent>(args.User) || HasComp<BeingCarriedComponent>(args.Target)
|
|
|| args.User == args.Target)
|
|
return;
|
|
|
|
AlternativeVerb verb = new()
|
|
{
|
|
Act = () =>
|
|
{
|
|
StartCarryDoAfter(args.User, uid, component);
|
|
},
|
|
Text = Loc.GetString("carry-verb"),
|
|
Priority = 2
|
|
};
|
|
args.Verbs.Add(verb);
|
|
}
|
|
|
|
private void AddInsertCarriedVerb(EntityUid uid, CarryingComponent component, GetVerbsEvent<InnateVerb> args)
|
|
{
|
|
// If the person is carrying someone, and the carried person is a pseudo-item, and the target entity is a storage,
|
|
// then add an action to insert the carried entity into the target
|
|
var toInsert = args.Using;
|
|
if (toInsert is not { Valid: true } || !args.CanAccess
|
|
|| !TryComp<PseudoItemComponent>(toInsert, out var pseudoItem)
|
|
|| !TryComp<StorageComponent>(args.Target, out var storageComp)
|
|
|| !_pseudoItem.CheckItemFits((toInsert.Value, pseudoItem), (args.Target, storageComp)))
|
|
return;
|
|
|
|
InnateVerb verb = new()
|
|
{
|
|
Act = () =>
|
|
{
|
|
DropCarried(uid, toInsert.Value);
|
|
_pseudoItem.TryInsert(args.Target, toInsert.Value, pseudoItem, storageComp);
|
|
},
|
|
Text = Loc.GetString("action-name-insert-other", ("target", toInsert)),
|
|
Priority = 2
|
|
};
|
|
args.Verbs.Add(verb);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Since the carried entity is stored as 2 virtual items, when deleted we want to drop them.
|
|
/// </summary>
|
|
private void OnVirtualItemDeleted(EntityUid uid, CarryingComponent component, VirtualItemDeletedEvent args)
|
|
{
|
|
if (!HasComp<CarriableComponent>(args.BlockingEntity))
|
|
return;
|
|
|
|
DropCarried(uid, args.BlockingEntity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Basically using virtual item passthrough to throw the carried person. A new age!
|
|
/// Maybe other things besides throwing should use virt items like this...
|
|
/// </summary>
|
|
private void OnThrow(EntityUid uid, CarryingComponent component, ref BeforeThrowEvent args)
|
|
{
|
|
if (!TryComp<VirtualItemComponent>(args.ItemUid, out var virtItem)
|
|
|| !HasComp<CarriableComponent>(virtItem.BlockingEntity))
|
|
return;
|
|
|
|
args.ItemUid = virtItem.BlockingEntity;
|
|
|
|
args.ThrowSpeed *= _contests.MassContest(uid, virtItem.BlockingEntity, false, 2f)
|
|
* _contests.StaminaContest(uid, virtItem.BlockingEntity);
|
|
}
|
|
|
|
private void OnParentChanged(EntityUid uid, CarryingComponent component, ref EntParentChangedMessage args)
|
|
{
|
|
var xform = Transform(uid);
|
|
if (xform.MapUid != args.OldMapId || xform.ParentUid == xform.GridUid)
|
|
return;
|
|
|
|
DropCarried(uid, component.Carried);
|
|
}
|
|
|
|
private void OnMobStateChanged(EntityUid uid, CarryingComponent component, MobStateChangedEvent args)
|
|
{
|
|
DropCarried(uid, component.Carried);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Only let the person being carried interact with their carrier and things on their person.
|
|
/// </summary>
|
|
private void OnInteractionAttempt(EntityUid uid, BeingCarriedComponent component, InteractionAttemptEvent args)
|
|
{
|
|
if (args.Target == null)
|
|
return;
|
|
|
|
var targetParent = Transform(args.Target.Value).ParentUid;
|
|
|
|
if (args.Target.Value != component.Carrier && targetParent != component.Carrier && targetParent != uid)
|
|
args.Cancel();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to escape via the escape inventory system.
|
|
/// </summary>
|
|
private void OnMoveInput(EntityUid uid, BeingCarriedComponent component, ref MoveInputEvent args)
|
|
{
|
|
if (!TryComp<CanEscapeInventoryComponent>(uid, out var escape)
|
|
|| !args.HasDirectionalMovement)
|
|
return;
|
|
|
|
// Check if the victim is in any way incapacitated, and if not make an escape attempt.
|
|
// Escape time scales with the inverse of a mass contest. Being lighter makes escape harder.
|
|
if (_actionBlockerSystem.CanInteract(uid, component.Carrier))
|
|
{
|
|
var disadvantage = _contests.MassContest(component.Carrier, uid, false, 2f);
|
|
_escapeInventorySystem.AttemptEscape(uid, component.Carrier, escape, disadvantage);
|
|
}
|
|
}
|
|
|
|
private void OnMoveAttempt(EntityUid uid, BeingCarriedComponent component, UpdateCanMoveEvent args)
|
|
{
|
|
args.Cancel();
|
|
}
|
|
|
|
private void OnStandAttempt(EntityUid uid, BeingCarriedComponent component, StandAttemptEvent args)
|
|
{
|
|
args.Cancel();
|
|
}
|
|
|
|
private void OnInteractedWith(EntityUid uid, BeingCarriedComponent component, GettingInteractedWithAttemptEvent args)
|
|
{
|
|
if (args.Uid != component.Carrier)
|
|
args.Cancel();
|
|
}
|
|
|
|
private void OnPullAttempt(EntityUid uid, BeingCarriedComponent component, PullAttemptEvent args)
|
|
{
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
private void OnStartClimb(EntityUid uid, BeingCarriedComponent component, ref StartClimbEvent args)
|
|
{
|
|
DropCarried(component.Carrier, uid);
|
|
}
|
|
|
|
private void OnBuckled(EntityUid uid, BeingCarriedComponent component, ref BuckledEvent args)
|
|
{
|
|
DropCarried(component.Carrier, uid);
|
|
}
|
|
|
|
private void OnDoAfter(EntityUid uid, CarriableComponent component, CarryDoAfterEvent args)
|
|
{
|
|
component.CancelToken = null;
|
|
if (args.Handled || args.Cancelled
|
|
|| !CanCarry(args.Args.User, uid, component))
|
|
return;
|
|
|
|
Carry(args.Args.User, uid);
|
|
args.Handled = true;
|
|
}
|
|
private void StartCarryDoAfter(EntityUid carrier, EntityUid carried, CarriableComponent component)
|
|
{
|
|
if (!TryComp<PhysicsComponent>(carrier, out var carrierPhysics)
|
|
|| !TryComp<PhysicsComponent>(carried, out var carriedPhysics)
|
|
|| carriedPhysics.Mass > carrierPhysics.Mass * 2f)
|
|
{
|
|
_popupSystem.PopupEntity(Loc.GetString("carry-too-heavy"), carried, carrier, Shared.Popups.PopupType.SmallCaution);
|
|
return;
|
|
}
|
|
|
|
var length = TimeSpan.FromSeconds(component.PickupDuration
|
|
* _contests.MassContest(carriedPhysics, carrierPhysics, false, 4f)
|
|
* _contests.StaminaContest(carrier, carried)
|
|
* (_standingState.IsDown(carried) ? 0.5f : 1));
|
|
|
|
component.CancelToken = new CancellationTokenSource();
|
|
|
|
var ev = new CarryDoAfterEvent();
|
|
var args = new DoAfterArgs(EntityManager, carrier, length, ev, carried, target: carried)
|
|
{
|
|
BreakOnMove = true,
|
|
NeedHand = true
|
|
};
|
|
|
|
_doAfterSystem.TryStartDoAfter(args);
|
|
|
|
// Show a popup to the person getting picked up
|
|
_popupSystem.PopupEntity(Loc.GetString("carry-started", ("carrier", carrier)), carried, carried);
|
|
}
|
|
|
|
private void Carry(EntityUid carrier, EntityUid carried)
|
|
{
|
|
if (TryComp<PullableComponent>(carried, out var pullable))
|
|
_pullingSystem.TryStopPull(carried, pullable);
|
|
|
|
_transform.AttachToGridOrMap(carrier);
|
|
_transform.AttachToGridOrMap(carried);
|
|
_transform.SetCoordinates(carried, Transform(carrier).Coordinates);
|
|
_transform.SetParent(carried, carrier);
|
|
_virtualItemSystem.TrySpawnVirtualItemInHand(carried, carrier);
|
|
_virtualItemSystem.TrySpawnVirtualItemInHand(carried, carrier);
|
|
var carryingComp = EnsureComp<CarryingComponent>(carrier);
|
|
ApplyCarrySlowdown(carrier, carried);
|
|
var carriedComp = EnsureComp<BeingCarriedComponent>(carried);
|
|
EnsureComp<KnockedDownComponent>(carried);
|
|
|
|
carryingComp.Carried = carried;
|
|
carriedComp.Carrier = carrier;
|
|
|
|
_actionBlockerSystem.UpdateCanMove(carried);
|
|
}
|
|
|
|
public bool TryCarry(EntityUid carrier, EntityUid toCarry, CarriableComponent? carriedComp = null)
|
|
{
|
|
if (!Resolve(toCarry, ref carriedComp, false)
|
|
|| !CanCarry(carrier, toCarry, carriedComp)
|
|
|| HasComp<BeingCarriedComponent>(carrier)
|
|
|| HasComp<ItemComponent>(carrier)
|
|
|| TryComp<PhysicsComponent>(carrier, out var carrierPhysics)
|
|
&& TryComp<PhysicsComponent>(toCarry, out var toCarryPhysics)
|
|
&& carrierPhysics.Mass < toCarryPhysics.Mass * 2f)
|
|
return false;
|
|
|
|
Carry(carrier, toCarry);
|
|
|
|
return true;
|
|
}
|
|
|
|
public void DropCarried(EntityUid carrier, EntityUid carried)
|
|
{
|
|
RemComp<CarryingComponent>(carrier); // get rid of this first so we don't recursively fire that event
|
|
RemComp<CarryingSlowdownComponent>(carrier);
|
|
RemComp<BeingCarriedComponent>(carried);
|
|
RemComp<KnockedDownComponent>(carried);
|
|
_actionBlockerSystem.UpdateCanMove(carried);
|
|
_virtualItemSystem.DeleteInHandsMatching(carrier, carried);
|
|
_transform.AttachToGridOrMap(carried);
|
|
_standingState.Stand(carried);
|
|
_movementSpeed.RefreshMovementSpeedModifiers(carrier);
|
|
}
|
|
|
|
private void ApplyCarrySlowdown(EntityUid carrier, EntityUid carried)
|
|
{
|
|
var massRatio = _contests.MassContest(carrier, carried, true);
|
|
var massRatioSq = MathF.Pow(massRatio, 2);
|
|
var modifier = 1 - 0.15f / massRatioSq;
|
|
modifier = Math.Max(0.1f, modifier);
|
|
|
|
var slowdownComp = EnsureComp<CarryingSlowdownComponent>(carrier);
|
|
_slowdown.SetModifier(carrier, modifier, modifier, slowdownComp);
|
|
}
|
|
|
|
public bool CanCarry(EntityUid carrier, EntityUid carried, CarriableComponent? carriedComp = null)
|
|
{
|
|
if (!Resolve(carried, ref carriedComp, false)
|
|
|| carriedComp.CancelToken != null
|
|
|| !HasComp<MapGridComponent>(Transform(carrier).ParentUid)
|
|
|| HasComp<BeingCarriedComponent>(carrier)
|
|
|| HasComp<BeingCarriedComponent>(carried)
|
|
|| !TryComp<HandsComponent>(carrier, out var hands)
|
|
|| hands.CountFreeHands() < carriedComp.FreeHandsRequired)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
var query = EntityQueryEnumerator<BeingCarriedComponent>();
|
|
while (query.MoveNext(out var carried, out var comp))
|
|
{
|
|
var carrier = comp.Carrier;
|
|
if (carrier is not { Valid: true } || carried is not { Valid: true })
|
|
continue;
|
|
|
|
// SOMETIMES - when an entity is inserted into disposals, or a cryosleep chamber - it can get re-parented without a proper reparent event
|
|
// when this happens, it needs to be dropped because it leads to weird behavior
|
|
if (Transform(carried).ParentUid != carrier)
|
|
{
|
|
DropCarried(carrier, carried);
|
|
continue;
|
|
}
|
|
|
|
// Make sure the carried entity is always centered relative to the carrier, as gravity pulls can offset it otherwise
|
|
var xform = Transform(carried);
|
|
if (!xform.LocalPosition.Equals(Vector2.Zero))
|
|
{
|
|
xform.LocalPosition = Vector2.Zero;
|
|
}
|
|
}
|
|
query.Dispose();
|
|
}
|
|
}
|
|
}
|