diff --git a/Content.Server/Resist/EscapeInventorySystem.cs b/Content.Server/Resist/EscapeInventorySystem.cs index 041257d78d75..020000bdb3dc 100644 --- a/Content.Server/Resist/EscapeInventorySystem.cs +++ b/Content.Server/Resist/EscapeInventorySystem.cs @@ -56,7 +56,7 @@ private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent componen AttemptEscape(uid, container.Owner, component); } - private void AttemptEscape(EntityUid user, EntityUid container, CanEscapeInventoryComponent component, float multiplier = 1f) + public void AttemptEscape(EntityUid user, EntityUid container, CanEscapeInventoryComponent component, float multiplier = 1f) { if (component.IsEscaping) return; diff --git a/Content.Server/Starshine/Carrying/CarryingSystem.cs b/Content.Server/Starshine/Carrying/CarryingSystem.cs new file mode 100644 index 000000000000..926df8bca2a1 --- /dev/null +++ b/Content.Server/Starshine/Carrying/CarryingSystem.cs @@ -0,0 +1,323 @@ +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.Starshine.Carrying.Components; +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.Starshine.Carrying; +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 Robust.Shared.Map.Components; +using Robust.Shared.Physics.Components; +using Robust.Server.GameObjects; + +namespace Content.Server.Starshine.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 TransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent>(AddCarryVerb); + SubscribeLocalEvent(OnVirtualItemDeleted); + SubscribeLocalEvent(OnThrow); + SubscribeLocalEvent(OnParentChanged); + SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnInteractionAttempt); + SubscribeLocalEvent(OnMoveInput); + SubscribeLocalEvent(OnMoveAttempt); + SubscribeLocalEvent(OnStandAttempt); + SubscribeLocalEvent(OnInteractedWith); + SubscribeLocalEvent(OnPullAttempt); + SubscribeLocalEvent(OnStartClimb); + SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnDoAfter); + } + + private void AddCarryVerb(EntityUid uid, CarriableComponent component, GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess || !_mobStateSystem.IsAlive(args.User) + || !CanCarry(args.User, uid, component) + || HasComp(args.User) + || HasComp(args.User) || HasComp(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); + } + + /// + /// Since the carried entity is stored as 2 virtual items, when deleted we want to drop them. + /// + private void OnVirtualItemDeleted(EntityUid uid, CarryingComponent component, VirtualItemDeletedEvent args) + { + if (!HasComp(args.BlockingEntity)) + return; + + DropCarried(uid, args.BlockingEntity); + } + + /// + /// Basically using virtual item passthrough to throw the carried person. A new age! + /// Maybe other things besides throwing should use virt items like this... + /// + private void OnThrow(EntityUid uid, CarryingComponent component, ref BeforeThrowEvent args) + { + if (!TryComp(args.ItemUid, out var virtItem) + || !TryComp(virtItem.BlockingEntity, out var carriedPhysics) + || !HasComp(virtItem.BlockingEntity)) + return; + + args.ItemUid = virtItem.BlockingEntity; + + var multiplier = carriedPhysics.Mass / 71.5f; + args.ThrowSpeed = 5f / multiplier; + } + + 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); + } + + /// + /// Only let the person being carried interact with their carrier and things on their person. + /// + 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.Cancelled = true; + } + + /// + /// Try to escape via the escape inventory system. + /// + private void OnMoveInput(EntityUid uid, BeingCarriedComponent component, ref MoveInputEvent args) + { + if (!TryComp(uid, out var escape) + || !args.HasDirectionalMovement) + return; + + // Check if the victim is in any way incapacitated, and if not make an escape attempt. + if (!_actionBlockerSystem.CanInteract(uid, component.Carrier)) + return; + + _escapeInventorySystem.AttemptEscape(uid, component.Carrier, escape, 0.5f); + } + + 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.Cancelled = true; + } + + 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 OnBuckleChange(EntityUid uid, BeingCarriedComponent component, TEvent 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) + { + var length = TimeSpan.FromSeconds(component.PickupDuration + * (_standingState.IsDown(carried) ? 0.5f : 1f)); + + 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, Shared.Popups.PopupType.MediumCaution); + } + + private void Carry(EntityUid carrier, EntityUid carried) + { + if (TryComp(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(carrier); + EnsureComp(carrier); + ApplyCarrySlowdown(carrier, carried); + var carriedComp = EnsureComp(carried); + EnsureComp(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(carrier) + || HasComp(carrier) + || TryComp(carrier, out var carrierPhysics) + && TryComp(toCarry, out var toCarryPhysics) + && carrierPhysics.Mass < toCarryPhysics.Mass * 2f) + return false; + + Carry(carrier, toCarry); + + return true; + } + + public void DropCarried(EntityUid carrier, EntityUid carried) + { + RemComp(carrier); // get rid of this first so we don't recursively fire that event + RemComp(carrier); + RemComp(carried); + RemComp(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 slowdownComp = EnsureComp(carrier); + _slowdown.SetModifier(carrier, slowdownComp.WalkModifier, slowdownComp.SprintModifier, slowdownComp); + } + + public bool CanCarry(EntityUid carrier, EntityUid carried, CarriableComponent? carriedComp = null) + { + return Resolve(carried, ref carriedComp, false) + && carriedComp.CancelToken == null + && HasComp(Transform(carrier).ParentUid) + && !HasComp(carrier) + && !HasComp(carried) + && TryComp(carrier, out var hands) + && hands.CountFreeHands() >= carriedComp.FreeHandsRequired; + } + + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + 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(); + } + } +} diff --git a/Content.Server/Starshine/Carrying/Components/BeingCarriedComponent.cs b/Content.Server/Starshine/Carrying/Components/BeingCarriedComponent.cs new file mode 100644 index 000000000000..5ff280b21d6e --- /dev/null +++ b/Content.Server/Starshine/Carrying/Components/BeingCarriedComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.Starshine.Carrying.Components +{ + /// + /// Stores the carrier of an entity being carried. + /// + [RegisterComponent] + public sealed partial class BeingCarriedComponent : Component + { + public EntityUid Carrier = default!; + } +} diff --git a/Content.Server/Starshine/Carrying/Components/CarriableComponent.cs b/Content.Server/Starshine/Carrying/Components/CarriableComponent.cs new file mode 100644 index 000000000000..122b9e8f9b7f --- /dev/null +++ b/Content.Server/Starshine/Carrying/Components/CarriableComponent.cs @@ -0,0 +1,23 @@ +using System.Threading; + +namespace Content.Server.Starshine.Carrying.Components +{ + [RegisterComponent] + public sealed partial class CarriableComponent : Component + { + public CancellationTokenSource? CancelToken; + /// + /// Number of free hands required + /// to carry the entity + /// + [DataField] + public int FreeHandsRequired = 2; + + /// + /// The base duration (In Seconds) of how long it should take to pick up this entity + /// before Contests are considered. + /// + [DataField] + public float PickupDuration = 4; + } +} diff --git a/Content.Server/Starshine/Carrying/Components/CarryingComponent.cs b/Content.Server/Starshine/Carrying/Components/CarryingComponent.cs new file mode 100644 index 000000000000..e0b9724486f1 --- /dev/null +++ b/Content.Server/Starshine/Carrying/Components/CarryingComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.Starshine.Carrying.Components +{ + /// + /// Added to an entity when they are carrying somebody. + /// + [RegisterComponent] + public sealed partial class CarryingComponent : Component + { + public EntityUid Carried = default!; + } +} diff --git a/Content.Shared/Starshine/Carrying/CarryingDoAfterEvent.cs b/Content.Shared/Starshine/Carrying/CarryingDoAfterEvent.cs new file mode 100644 index 000000000000..78a890cb8dca --- /dev/null +++ b/Content.Shared/Starshine/Carrying/CarryingDoAfterEvent.cs @@ -0,0 +1,8 @@ +using Robust.Shared.Serialization; +using Content.Shared.DoAfter; + +namespace Content.Shared.Starshine.Carrying +{ + [Serializable, NetSerializable] + public sealed partial class CarryDoAfterEvent : SimpleDoAfterEvent; +} diff --git a/Content.Shared/Starshine/Carrying/CarryingSlowdownComponent.cs b/Content.Shared/Starshine/Carrying/CarryingSlowdownComponent.cs new file mode 100644 index 000000000000..a64d3bd60c21 --- /dev/null +++ b/Content.Shared/Starshine/Carrying/CarryingSlowdownComponent.cs @@ -0,0 +1,28 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Starshine.Carrying +{ + [RegisterComponent, NetworkedComponent, Access(typeof(CarryingSlowdownSystem))] + + public sealed partial class CarryingSlowdownComponent : Component + { + [DataField(required: true)] + public float WalkModifier = 0.9f; + + [DataField(required: true)] + public float SprintModifier = 0.9f; + } + + [Serializable, NetSerializable] + public sealed class CarryingSlowdownComponentState : ComponentState + { + public float WalkModifier; + public float SprintModifier; + public CarryingSlowdownComponentState(float walkModifier, float sprintModifier) + { + WalkModifier = walkModifier; + SprintModifier = sprintModifier; + } + } +} diff --git a/Content.Shared/Starshine/Carrying/CarryingSlowdownSystem.cs b/Content.Shared/Starshine/Carrying/CarryingSlowdownSystem.cs new file mode 100644 index 000000000000..785eaf833477 --- /dev/null +++ b/Content.Shared/Starshine/Carrying/CarryingSlowdownSystem.cs @@ -0,0 +1,46 @@ +using Content.Shared.Movement.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Starshine.Carrying +{ + public sealed class CarryingSlowdownSystem : EntitySystem + { + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnRefreshMoveSpeed); + } + + public void SetModifier(EntityUid uid, float walkSpeedModifier, float sprintSpeedModifier, CarryingSlowdownComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.WalkModifier = walkSpeedModifier; + component.SprintModifier = sprintSpeedModifier; + _movementSpeed.RefreshMovementSpeedModifiers(uid); + } + private void OnGetState(EntityUid uid, CarryingSlowdownComponent component, ref ComponentGetState args) + { + args.State = new CarryingSlowdownComponentState(component.WalkModifier, component.SprintModifier); + } + + private void OnHandleState(EntityUid uid, CarryingSlowdownComponent component, ref ComponentHandleState args) + { + if (args.Current is not CarryingSlowdownComponentState state) + return; + + component.WalkModifier = state.WalkModifier; + component.SprintModifier = state.SprintModifier; + _movementSpeed.RefreshMovementSpeedModifiers(uid); + } + private void OnRefreshMoveSpeed(EntityUid uid, CarryingSlowdownComponent component, RefreshMovementSpeedModifiersEvent args) + { + args.ModifySpeed(component.WalkModifier, component.SprintModifier); + } + } +} diff --git a/Resources/Changelog/ChangelogStarshine.yml b/Resources/Changelog/ChangelogStarshine.yml index f254c30b372e..12babd931a72 100644 --- a/Resources/Changelog/ChangelogStarshine.yml +++ b/Resources/Changelog/ChangelogStarshine.yml @@ -2164,3 +2164,10 @@ id: 164 time: '2024-10-15T21:30:15.0000000+00:00' url: null +- author: MilenVolf + changes: + - message: "Добавлена возможность переноски людей на руках!" + type: Add + id: 165 + time: '2024-10-15T21:50:45.0000000+00:00' + url: null diff --git a/Resources/Locale/en-US/starshine/carrying/carry.ftl b/Resources/Locale/en-US/starshine/carrying/carry.ftl new file mode 100644 index 000000000000..e07afa4336d2 --- /dev/null +++ b/Resources/Locale/en-US/starshine/carrying/carry.ftl @@ -0,0 +1,2 @@ +carry-verb = Carry +carry-started = {THE($carrier)} is trying to pick you up! diff --git a/Resources/Locale/ru-RU/starshine/carrying/carry.ftl b/Resources/Locale/ru-RU/starshine/carrying/carry.ftl new file mode 100644 index 000000000000..b85a386d2dd5 --- /dev/null +++ b/Resources/Locale/ru-RU/starshine/carrying/carry.ftl @@ -0,0 +1,2 @@ +carry-verb = Нести +carry-started = {$carrier} пытается взять вас на руки! diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 100a0ced8427..ca5dfda44f66 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -211,6 +211,10 @@ - FootstepSound - DoorBumpOpener - AnomalyHost +## Starshine-start + - type: CanEscapeInventory + - type: Carriable +## Starshine-end - type: entity save: false