using Content.Server.DoAfter; using Content.Server.Popups; using Content.Shared.ActionBlocker; using Content.Shared.Body.Components; using Content.Shared.Body.Part; using Content.Shared.Climbing; using Content.Shared.DragDrop; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Popups; using Robust.Shared.Physics; namespace Content.Server.Climbing.Components { [RegisterComponent] [ComponentReference(typeof(IClimbable))] public sealed class ClimbableComponent : SharedClimbableComponent { [Dependency] private readonly IEntityManager _entities = default!; /// /// The time it takes to climb onto the entity. /// [ViewVariables] [DataField("delay")] private float _climbDelay = 0.8f; protected override void Initialize() { base.Initialize(); if (!Owner.EnsureComponent(out PhysicsComponent _)) { Logger.Warning($"Entity {_entities.GetComponent(Owner).EntityName} at {_entities.GetComponent(Owner).MapPosition} didn't have a {nameof(PhysicsComponent)}"); } } public override bool CanDragDropOn(DragDropEvent eventArgs) { if (!base.CanDragDropOn(eventArgs)) return false; string reason; bool canVault; if (eventArgs.User == eventArgs.Dragged) canVault = CanVault(eventArgs.User, eventArgs.Target, out reason); else canVault = CanVault(eventArgs.User, eventArgs.Dragged, eventArgs.Target, out reason); if (!canVault) eventArgs.User.PopupMessage(reason); return canVault; } /// /// Checks if the user can vault the target /// /// The entity that wants to vault /// The object that is being vaulted /// The reason why it cant be dropped /// private bool CanVault(EntityUid user, EntityUid target, out string reason) { if (!EntitySystem.Get().CanInteract(user, target)) { reason = Loc.GetString("comp-climbable-cant-interact"); return false; } if (!_entities.HasComponent(user) || !_entities.TryGetComponent(user, out SharedBodyComponent? body)) { reason = Loc.GetString("comp-climbable-cant-climb"); return false; } if (!body.HasPartOfType(BodyPartType.Leg) || !body.HasPartOfType(BodyPartType.Foot)) { reason = Loc.GetString("comp-climbable-cant-climb"); return false; } if (!EntitySystem.Get().InRangeUnobstructed(user, target, Range)) { reason = Loc.GetString("comp-climbable-cant-reach"); return false; } reason = string.Empty; return true; } /// /// Checks if the user can vault the dragged entity onto the the target /// /// The user that wants to vault the entity /// The entity that is being vaulted /// The object that is being vaulted onto /// The reason why it cant be dropped /// private bool CanVault(EntityUid user, EntityUid dragged, EntityUid target, out string reason) { if (!EntitySystem.Get().CanInteract(user, dragged)) { reason = Loc.GetString("comp-climbable-cant-interact"); return false; } // CanInteract() doesn't support checking a second "target" entity. // Doing so manually: var ev = new GettingInteractedWithAttemptEvent(user, target); _entities.EventBus.RaiseLocalEvent(target, ev); if (ev.Cancelled) { reason = Loc.GetString("comp-climbable-cant-interact"); return false; } if (!_entities.HasComponent(dragged)) { reason = Loc.GetString("comp-climbable-cant-climb"); return false; } bool Ignored(EntityUid entity) => entity == target || entity == user || entity == dragged; var sys = EntitySystem.Get(); if (!sys.InRangeUnobstructed(user, target, Range, predicate: Ignored) || !sys.InRangeUnobstructed(user, dragged, Range, predicate: Ignored)) { reason = Loc.GetString("comp-climbable-cant-reach"); return false; } reason = string.Empty; return true; } public override bool DragDropOn(DragDropEvent eventArgs) { if (eventArgs.User == eventArgs.Dragged) { TryClimb(eventArgs.User, eventArgs.Target); } else { TryMoveEntity(eventArgs.User, eventArgs.Dragged, eventArgs.Target); } return true; } private async void TryMoveEntity(EntityUid user, EntityUid entityToMove, EntityUid climbable) { var doAfterEventArgs = new DoAfterEventArgs(user, _climbDelay, default, entityToMove) { BreakOnTargetMove = true, BreakOnUserMove = true, BreakOnDamage = true, BreakOnStun = true }; var result = await EntitySystem.Get().WaitDoAfter(doAfterEventArgs); if (result != DoAfterStatus.Cancelled && _entities.TryGetComponent(entityToMove, out FixturesComponent? body) && body.FixtureCount >= 1) { var entityPos = _entities.GetComponent(entityToMove).WorldPosition; var direction = (_entities.GetComponent(Owner).WorldPosition - entityPos).Normalized; var endPoint = _entities.GetComponent(Owner).WorldPosition; var climbMode = _entities.GetComponent(entityToMove); climbMode.IsClimbing = true; if (MathF.Abs(direction.X) < 0.6f) // user climbed mostly vertically so lets make it a clean straight line { endPoint = new Vector2(entityPos.X, endPoint.Y); } else if (MathF.Abs(direction.Y) < 0.6f) // user climbed mostly horizontally so lets make it a clean straight line { endPoint = new Vector2(endPoint.X, entityPos.Y); } climbMode.TryMoveTo(entityPos, endPoint); // we may potentially need additional logic since we're forcing a player onto a climbable // there's also the cases where the user might collide with the person they are forcing onto the climbable that i haven't accounted for _entities.EventBus.RaiseLocalEvent(entityToMove, new StartClimbEvent(climbable), false); _entities.EventBus.RaiseLocalEvent(climbable, new ClimbedOnEvent(entityToMove), false); var othersMessage = Loc.GetString("comp-climbable-user-climbs-force-other", ("user", user), ("moved-user", entityToMove), ("climbable", Owner)); user.PopupMessageOtherClients(othersMessage); var selfMessage = Loc.GetString("comp-climbable-user-climbs-force", ("moved-user", entityToMove), ("climbable", Owner)); user.PopupMessage(selfMessage); } } public async void TryClimb(EntityUid user, EntityUid climbable) { if (!_entities.TryGetComponent(user, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing) return; var doAfterEventArgs = new DoAfterEventArgs(user, _climbDelay, default, Owner) { BreakOnTargetMove = true, BreakOnUserMove = true, BreakOnDamage = true, BreakOnStun = true }; var result = await EntitySystem.Get().WaitDoAfter(doAfterEventArgs); if (result != DoAfterStatus.Cancelled && _entities.TryGetComponent(user, out FixturesComponent? fixtureComp) && fixtureComp.Fixtures.Count >= 1) { // TODO: Remove the copy-paste code var userPos = _entities.GetComponent(user).WorldPosition; var direction = (_entities.GetComponent(Owner).WorldPosition - userPos).Normalized; var endPoint = _entities.GetComponent(Owner).WorldPosition; _entities.EventBus.RaiseLocalEvent(user, new StartClimbEvent(climbable), false); _entities.EventBus.RaiseLocalEvent(climbable, new ClimbedOnEvent(user), false); var climbMode = _entities.GetComponent(user); climbMode.IsClimbing = true; if (MathF.Abs(direction.X) < 0.6f) // user climbed mostly vertically so lets make it a clean straight line { endPoint = new Vector2(_entities.GetComponent(user).WorldPosition.X, endPoint.Y); } else if (MathF.Abs(direction.Y) < 0.6f) // user climbed mostly horizontally so lets make it a clean straight line { endPoint = new Vector2(endPoint.X, _entities.GetComponent(user).WorldPosition.Y); } climbMode.TryMoveTo(userPos, endPoint); var othersMessage = Loc.GetString("comp-climbable-user-climbs-other", ("user", user), ("climbable", Owner)); user.PopupMessageOtherClients(othersMessage); var selfMessage = Loc.GetString("comp-climbable-user-climbs", ("climbable", Owner)); user.PopupMessage(selfMessage); } } } } /// /// Raised on an entity when it is climbed on. /// public sealed class ClimbedOnEvent : EntityEventArgs { public EntityUid Climber; public ClimbedOnEvent(EntityUid climber) { Climber = climber; } } /// /// Raised on an entity when it successfully climbs on something. /// public sealed class StartClimbEvent : EntityEventArgs { public EntityUid Climbable; public StartClimbEvent(EntityUid climbable) { Climbable = climbable; } }