diff --git a/Content.Client/Climbing/ClimbingSystem.cs b/Content.Client/Climbing/ClimbingSystem.cs deleted file mode 100644 index 99f7a4b4f662df..00000000000000 --- a/Content.Client/Climbing/ClimbingSystem.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Shared.Climbing; - -namespace Content.Client.Climbing -{ - public sealed class ClimbingSystem : SharedClimbSystem - { - - } -} diff --git a/Content.Client/Movement/ClimbSystem.cs b/Content.Client/Movement/ClimbSystem.cs new file mode 100644 index 00000000000000..f8b1afc3cf0eb1 --- /dev/null +++ b/Content.Client/Movement/ClimbSystem.cs @@ -0,0 +1,44 @@ +using Content.Client.Interactable; +using Content.Client.Movement.Components; +using Content.Shared.Climbing; +using Content.Shared.DragDrop; +using Robust.Shared.GameStates; + +namespace Content.Client.Movement; + +public sealed class ClimbSystem : SharedClimbSystem +{ + [Dependency] private readonly InteractionSystem _interactionSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnClimbingState); + } + + private static void OnClimbingState(EntityUid uid, ClimbingComponent component, ref ComponentHandleState args) + { + if (args.Current is not SharedClimbingComponent.ClimbModeComponentState climbModeState) + return; + + component.IsClimbing = climbModeState.Climbing; + component.OwnerIsTransitioning = climbModeState.IsTransitioning; + } + + protected override void OnCanDragDropOn(EntityUid uid, SharedClimbableComponent component, CanDragDropOnEvent args) + { + base.OnCanDragDropOn(uid, component, args); + + if (!args.CanDrop) + return; + + var user = args.User; + var target = args.Target; + var dragged = args.Dragged; + bool Ignored(EntityUid entity) => entity == target || entity == user || entity == dragged; + + args.CanDrop = _interactionSystem.InRangeUnobstructed(user, target, component.Range, predicate: Ignored) + && _interactionSystem.InRangeUnobstructed(user, dragged, component.Range, predicate: Ignored); + args.Handled = true; + } +} diff --git a/Content.Client/Movement/Components/ClimbableComponent.cs b/Content.Client/Movement/Components/ClimbableComponent.cs index 9d92566a0a7299..dd06d5296958b7 100644 --- a/Content.Client/Movement/Components/ClimbableComponent.cs +++ b/Content.Client/Movement/Components/ClimbableComponent.cs @@ -1,34 +1,8 @@ using Content.Shared.Climbing; -using Content.Shared.DragDrop; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Helpers; -using Robust.Shared.GameObjects; -namespace Content.Client.Movement.Components -{ - [RegisterComponent] - [ComponentReference(typeof(IClimbable))] - public sealed class ClimbableComponent : SharedClimbableComponent - { - public override bool CanDragDropOn(DragDropEvent eventArgs) - { - if (!base.CanDragDropOn(eventArgs)) - return false; +namespace Content.Client.Movement.Components; - var user = eventArgs.User; - var target = eventArgs.Target; - var dragged = eventArgs.Dragged; - bool Ignored(EntityUid entity) => entity == target || entity == user || entity == dragged; - - var sys = EntitySystem.Get(); - - return sys.InRangeUnobstructed(user, target, Range, predicate: Ignored) - && sys.InRangeUnobstructed(user, dragged, Range, predicate: Ignored); - } - - public override bool DragDropOn(DragDropEvent eventArgs) - { - return false; - } - } -} +[RegisterComponent] +[Friend(typeof(ClimbSystem))] +[ComponentReference(typeof(SharedClimbableComponent))] +public sealed class ClimbableComponent : SharedClimbableComponent { } diff --git a/Content.Client/Movement/Components/ClimbingComponent.cs b/Content.Client/Movement/Components/ClimbingComponent.cs index 47abebcb5227f3..7891a6ee2137cc 100644 --- a/Content.Client/Movement/Components/ClimbingComponent.cs +++ b/Content.Client/Movement/Components/ClimbingComponent.cs @@ -1,23 +1,8 @@ using Content.Shared.Climbing; -using Robust.Shared.GameObjects; -namespace Content.Client.Movement.Components -{ - [RegisterComponent] - [ComponentReference(typeof(SharedClimbingComponent))] - public sealed class ClimbingComponent : SharedClimbingComponent - { - public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) - { - base.HandleComponentState(curState, nextState); +namespace Content.Client.Movement.Components; - if (curState is not ClimbModeComponentState climbModeState) - { - return; - } - - IsClimbing = climbModeState.Climbing; - OwnerIsTransitioning = climbModeState.IsTransitioning; - } - } -} +[RegisterComponent] +[Friend(typeof(ClimbSystem))] +[ComponentReference(typeof(SharedClimbingComponent))] +public sealed class ClimbingComponent : SharedClimbingComponent { } diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs index 2b0df4f8b2a323..e9e47136973ac6 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs @@ -7,6 +7,7 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Physics; +using Content.Server.Climbing; namespace Content.IntegrationTests.Tests.GameObjects.Components.Movement { @@ -59,7 +60,7 @@ public async Task Test() // Now let's make the player enter a climbing transitioning state. climbing.IsClimbing = true; - climbing.TryMoveTo(entityManager.GetComponent(human).WorldPosition, entityManager.GetComponent(table).WorldPosition); + EntitySystem.Get().MoveEntityToward(human, table, climbing:climbing); var body = entityManager.GetComponent(human); // TODO: Check it's climbing diff --git a/Content.Server/Climbing/ClimbSystem.cs b/Content.Server/Climbing/ClimbSystem.cs index 104c0f73147c7e..aa48714f7047c6 100644 --- a/Content.Server/Climbing/ClimbSystem.cs +++ b/Content.Server/Climbing/ClimbSystem.cs @@ -1,121 +1,421 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Content.Server.Climbing.Components; +using Content.Server.DoAfter; using Content.Server.Popups; using Content.Server.Stunnable; using Content.Shared.ActionBlocker; +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; using Content.Shared.Buckle.Components; using Content.Shared.Climbing; using Content.Shared.Damage; +using Content.Shared.DragDrop; using Content.Shared.GameTicking; +using Content.Shared.Interaction; +using Content.Shared.Physics; +using Content.Shared.Popups; using Content.Shared.Verbs; using JetBrains.Annotations; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; +using Robust.Shared.GameStates; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Collision.Shapes; +using Robust.Shared.Physics.Dynamics; using Robust.Shared.Player; -namespace Content.Server.Climbing +namespace Content.Server.Climbing; + +[UsedImplicitly] +public sealed class ClimbSystem : SharedClimbSystem { - [UsedImplicitly] - internal sealed class ClimbSystem : SharedClimbSystem + [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly FixtureSystem _fixtureSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; + [Dependency] private readonly StunSystem _stunSystem = default!; + + private const string ClimbingFixtureName = "climb"; + private const int ClimbingCollisionGroup = (int) CollisionGroup.VaultImpassable; + + private readonly Dictionary> _fixtureRemoveQueue = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(Reset); + SubscribeLocalEvent>(AddClimbableVerb); + SubscribeLocalEvent(OnClimbableDragDrop); + + SubscribeLocalEvent(OnClimbFinished); + SubscribeLocalEvent(OnClimbEndCollide); + SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnClimbingGetState); + + SubscribeLocalEvent(OnGlassClimbed); + } + + protected override void OnCanDragDropOn(EntityUid uid, SharedClimbableComponent component, CanDragDropOnEvent args) + { + base.OnCanDragDropOn(uid, component, args); + + if (!args.CanDrop) + return; + + string reason; + var canVault = args.User == args.Dragged + ? CanVault(component, args.User, args.Target, out reason) + : CanVault(component, args.User, args.Dragged, args.Target, out reason); + + if (!canVault) + _popupSystem.PopupEntity(reason, args.User, Filter.Entities(args.User)); + + args.CanDrop = canVault; + args.Handled = true; + } + + private void AddClimbableVerb(EntityUid uid, ClimbableComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanMove(args.User)) + return; + + if (!TryComp(args.User, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing) + return; + + // TODO VERBS ICON add a climbing icon? + args.Verbs.Add(new AlternativeVerb + { + Act = () => TryMoveEntity(component, args.User, args.User, args.Target), + Text = Loc.GetString("comp-climbable-verb-climb") + }); + } + + private void OnClimbableDragDrop(EntityUid uid, ClimbableComponent component, DragDropEvent args) { - private readonly HashSet _activeClimbers = new(); + TryMoveEntity(component, args.User, args.Dragged, args.Target); + } - [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; - [Dependency] private readonly StunSystem _stunSystem = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; + private void TryMoveEntity(ClimbableComponent component, EntityUid user, EntityUid entityToMove, + EntityUid climbable) + { + if (!TryComp(entityToMove, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing) + return; - public override void Initialize() + _doAfterSystem.DoAfter(new DoAfterEventArgs(entityToMove, component.ClimbDelay, default, climbable) { - base.Initialize(); + BreakOnTargetMove = true, + BreakOnUserMove = true, + BreakOnDamage = true, + BreakOnStun = true, + UserFinishedEvent = new ClimbFinishedEvent(user, climbable) + }); + } + + private void OnClimbFinished(EntityUid uid, ClimbingComponent climbingComp, ClimbFinishedEvent args) + { + if (!TryComp(uid, out var physicsComp) + || !TryComp(uid, out var fixturesComp)) + return; + + if (!ReplaceFixtures(climbingComp, physicsComp, fixturesComp)) + return; + + climbingComp.IsClimbing = true; - SubscribeLocalEvent(Reset); - SubscribeLocalEvent>(AddClimbVerb); - SubscribeLocalEvent(OnBuckleChange); - SubscribeLocalEvent(OnGlassClimbed); + MoveEntityToward(uid, args.Climbable, physicsComp, climbingComp); + // 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 + + RaiseLocalEvent(uid, new StartClimbEvent(args.Climbable), false); + RaiseLocalEvent(args.Climbable, new ClimbedOnEvent(uid), false); + + if (args.User == uid) + { + var othersMessage = Loc.GetString("comp-climbable-user-climbs-other", ("user", uid), + ("climbable", args.Climbable)); + uid.PopupMessageOtherClients(othersMessage); + + var selfMessage = Loc.GetString("comp-climbable-user-climbs", ("climbable", args.Climbable)); + uid.PopupMessage(selfMessage); + } + else + { + var othersMessage = Loc.GetString("comp-climbable-user-climbs-force-other", ("user", args.User), + ("moved-user", uid), ("climbable", args.Climbable)); + args.User.PopupMessageOtherClients(othersMessage); + + var selfMessage = Loc.GetString("comp-climbable-user-climbs-force", ("moved-user", uid), + ("climbable", args.Climbable)); + args.User.PopupMessage(selfMessage); } + } - public void ForciblySetClimbing(EntityUid uid, ClimbingComponent? component = null) + /// + /// Replaces the current fixtures with non-climbing collidable versions so that climb end can be detected + /// + /// Returns whether adding the new fixtures was successful + private bool ReplaceFixtures(ClimbingComponent climbingComp, PhysicsComponent physicsComp, FixturesComponent fixturesComp) + { + // Swap fixtures + foreach (var (name, fixture) in fixturesComp.Fixtures) { - if (!Resolve(uid, ref component, false)) - return; - component.IsClimbing = true; - UnsetTransitionBoolAfterBufferTime(uid, component); + if (climbingComp.DisabledFixtureMasks.ContainsKey(name) + || fixture.Hard == false + || (fixture.CollisionMask & ClimbingCollisionGroup) == 0) + continue; + + climbingComp.DisabledFixtureMasks.Add(fixture.ID, fixture.CollisionMask & ClimbingCollisionGroup); + fixture.CollisionMask &= ~ClimbingCollisionGroup; } - private void AddClimbVerb(EntityUid uid, ClimbableComponent component, GetVerbsEvent args) + if (!_fixtureSystem.TryCreateFixture(physicsComp, + new Fixture(new PhysShapeCircle { Radius = 0.35f }, (int) CollisionGroup.None, ClimbingCollisionGroup, false) + {ID = ClimbingFixtureName}, manager: fixturesComp)) + return false; + return true; + } + + private void OnClimbEndCollide(EntityUid uid, ClimbingComponent component, EndCollideEvent args) + { + if (args.OurFixture.ID != ClimbingFixtureName + || !component.IsClimbing + || component.OwnerIsTransitioning + || !TryComp(uid, out var transformComp) + || !TryComp(uid, out var physicsComp) + || !TryComp(uid, out var fixturesComp)) + return; + + foreach (var fixture in args.OurFixture.Contacts.Keys) { - if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanMove(args.User)) + if (fixture == args.OtherFixture) + continue; + // If still colliding with a climbable, do not stop climbing + if (HasComp(fixture.Body.Owner)) return; + } - // Check that the user climb. - if (!EntityManager.TryGetComponent(args.User, out ClimbingComponent? climbingComponent) || - climbingComponent.IsClimbing) - return; + foreach (var (name, fixtureMask) in component.DisabledFixtureMasks) + { + if (!fixturesComp.Fixtures.TryGetValue(name, out var fixture)) + continue; + fixture.CollisionMask |= fixtureMask; + } + component.DisabledFixtureMasks.Clear(); - // Add a climb verb - AlternativeVerb verb = new(); - verb.Act = () => component.TryClimb(args.User, args.Target); - verb.Text = Loc.GetString("comp-climbable-verb-climb"); - // TODO VERBS ICON add a climbing icon? - args.Verbs.Add(verb); + if (!_fixtureRemoveQueue.TryGetValue(uid, out var removeQueue)) + { + removeQueue = new List(); + _fixtureRemoveQueue.Add(uid, removeQueue); } - private void OnBuckleChange(EntityUid uid, ClimbingComponent component, BuckleChangeEvent args) + if (fixturesComp.Fixtures.TryGetValue(ClimbingFixtureName, out var climbingFixture)) + removeQueue.Add(climbingFixture); + + component.IsClimbing = false; + } + + /// + /// Checks if the user can vault the target + /// + /// The component of the entity that is being vaulted + /// The entity that wants to vault + /// The object that is being vaulted + /// The reason why it cant be dropped + /// + private bool CanVault(SharedClimbableComponent component, EntityUid user, EntityUid target, out string reason) + { + if (!_actionBlockerSystem.CanInteract(user, target)) { - if (args.Buckling) - component.IsClimbing = false; + reason = Loc.GetString("comp-climbable-cant-interact"); + return false; } - private void OnGlassClimbed(EntityUid uid, GlassTableComponent component, ClimbedOnEvent args) + if (!HasComp(user) + || !TryComp(user, out SharedBodyComponent? body) + || !body.HasPartOfType(BodyPartType.Leg) + || !body.HasPartOfType(BodyPartType.Foot)) { - if (TryComp(args.Climber, out var physics) && physics.Mass <= component.MassLimit) - return; - _damageableSystem.TryChangeDamage(args.Climber, component.ClimberDamage); - _damageableSystem.TryChangeDamage(uid, component.TableDamage); - _stunSystem.TryParalyze(args.Climber, TimeSpan.FromSeconds(component.StunTime), true); - - // Not shown to the user, since they already get a 'you climb on the glass table' popup - _popupSystem.PopupEntity(Loc.GetString("glass-table-shattered-others", - ("table", uid), ("climber", args.Climber)), args.Climber, - Filter.Pvs(uid).RemoveWhereAttachedEntity(puid => puid == args.Climber)); + reason = Loc.GetString("comp-climbable-cant-climb"); + return false; } - public void AddActiveClimber(ClimbingComponent climbingComponent) + if (!_interactionSystem.InRangeUnobstructed(user, target, component.Range)) { - _activeClimbers.Add(climbingComponent); + reason = Loc.GetString("comp-climbable-cant-reach"); + return false; } - public void RemoveActiveClimber(ClimbingComponent climbingComponent) + reason = string.Empty; + return true; + } + + /// + /// Checks if the user can vault the dragged entity onto the the target + /// + /// The climbable component of the object being vaulted onto + /// 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(SharedClimbableComponent component, EntityUid user, EntityUid dragged, EntityUid target, + out string reason) + { + if (!_actionBlockerSystem.CanInteract(user, dragged) || !_actionBlockerSystem.CanInteract(user, target)) { - _activeClimbers.Remove(climbingComponent); + reason = Loc.GetString("comp-climbable-cant-interact"); + return false; } - public void UnsetTransitionBoolAfterBufferTime(EntityUid uid, ClimbingComponent? component = null) + if (!HasComp(dragged)) { - if (!Resolve(uid, ref component, false)) - return; - component.Owner.SpawnTimer((int) (SharedClimbingComponent.BufferTime * 1000), () => - { - if (component.Deleted) return; - component.OwnerIsTransitioning = false; - }); + reason = Loc.GetString("comp-climbable-cant-climb"); + return false; } - public override void Update(float frameTime) + bool Ignored(EntityUid entity) => entity == target || entity == user || entity == dragged; + + if (!_interactionSystem.InRangeUnobstructed(user, target, component.Range, predicate: Ignored) + || !_interactionSystem.InRangeUnobstructed(user, dragged, component.Range, predicate: Ignored)) { - foreach (var climber in _activeClimbers.ToArray()) - { - climber.Update(); - } + reason = Loc.GetString("comp-climbable-cant-reach"); + return false; } - public void Reset(RoundRestartCleanupEvent ev) + reason = string.Empty; + return true; + } + + public void ForciblySetClimbing(EntityUid uid, ClimbingComponent? component = null) + { + if (!Resolve(uid, ref component, false)) + return; + component.IsClimbing = true; + } + + private static void OnBuckleChange(EntityUid uid, ClimbingComponent component, BuckleChangeEvent args) + { + if (!args.Buckling) + return; + component.IsClimbing = false; + component.OwnerIsTransitioning = false; + } + + private static void OnClimbingGetState(EntityUid uid, ClimbingComponent component, ref ComponentGetState args) + { + args.State = new SharedClimbingComponent.ClimbModeComponentState(component.IsClimbing, component.OwnerIsTransitioning); + } + + private void OnGlassClimbed(EntityUid uid, GlassTableComponent component, ClimbedOnEvent args) + { + if (TryComp(args.Climber, out var physics) && physics.Mass <= component.MassLimit) + return; + + _damageableSystem.TryChangeDamage(args.Climber, component.ClimberDamage); + _damageableSystem.TryChangeDamage(uid, component.TableDamage); + _stunSystem.TryParalyze(args.Climber, TimeSpan.FromSeconds(component.StunTime), true); + + // Not shown to the user, since they already get a 'you climb on the glass table' popup + _popupSystem.PopupEntity( + Loc.GetString("glass-table-shattered-others", ("table", uid), ("climber", args.Climber)), args.Climber, + Filter.Pvs(uid).RemoveWhereAttachedEntity(puid => puid == args.Climber)); + } + + /// + /// Moves the entity toward the target climbed entity + /// + public void MoveEntityToward(EntityUid uid, EntityUid target, PhysicsComponent? physics = null, ClimbingComponent? climbing = null) + { + if (!Resolve(uid, ref physics, ref climbing, false)) + return; + + var from = Transform(uid).WorldPosition; + var to = Transform(target).WorldPosition; + var (x, y) = (to - from).Normalized; + + if (MathF.Abs(x) < 0.6f) // user climbed mostly vertically so lets make it a clean straight line + to = new Vector2(from.X, to.Y); + else if (MathF.Abs(y) < 0.6f) // user climbed mostly horizontally so lets make it a clean straight line + to = new Vector2(to.X, from.Y); + + var velocity = (to - from).Length; + + if (velocity <= 0.0f) return; + + // Since there are bodies with different masses: + // mass * 10 seems enough to move entity + // instead of launching cats like rockets against the walls with constant impulse value. + physics.ApplyLinearImpulse((to - from).Normalized * velocity * physics.Mass * 10); + physics.BodyType = BodyType.Dynamic; + climbing.OwnerIsTransitioning = true; + _actionBlockerSystem.UpdateCanMove(uid); + + // Transition back to KinematicController after BufferTime + climbing.Owner.SpawnTimer((int) (SharedClimbingComponent.BufferTime * 1000), () => { - _activeClimbers.Clear(); + if (climbing.Deleted) return; + physics.BodyType = BodyType.KinematicController; + climbing.OwnerIsTransitioning = false; + _actionBlockerSystem.UpdateCanMove(uid); + }); + } + + public override void Update(float frameTime) + { + foreach (var (uid, fixtures) in _fixtureRemoveQueue) + { + if (!TryComp(uid, out var physicsComp) + || !TryComp(uid, out var fixturesComp)) + continue; + foreach (var fixture in fixtures) + { + _fixtureSystem.DestroyFixture(physicsComp, fixture, true, fixturesComp); + } } + + _fixtureRemoveQueue.Clear(); + } + + private void Reset(RoundRestartCleanupEvent ev) + { + _fixtureRemoveQueue.Clear(); } } + +internal sealed class ClimbFinishedEvent : EntityEventArgs +{ + public ClimbFinishedEvent(EntityUid user, EntityUid climbable) + { + User = user; + Climbable = climbable; + } + + public EntityUid User { get; } + public EntityUid Climbable { get; } +} + +/// +/// 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; + } +} \ No newline at end of file diff --git a/Content.Server/Climbing/Components/ClimbableComponent.cs b/Content.Server/Climbing/Components/ClimbableComponent.cs index 1246c36c188bcd..f87334e5cc3eae 100644 --- a/Content.Server/Climbing/Components/ClimbableComponent.cs +++ b/Content.Server/Climbing/Components/ClimbableComponent.cs @@ -1,277 +1,14 @@ -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; +namespace Content.Server.Climbing.Components; - 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 +[RegisterComponent] +public sealed class ClimbableComponent : SharedClimbableComponent { - public EntityUid Climbable; - - public StartClimbEvent(EntityUid climbable) - { - Climbable = climbable; - } + /// + /// The time it takes to climb onto the entity. + /// + [ViewVariables] + [DataField("delay")] + public float ClimbDelay = 0.8f; } diff --git a/Content.Server/Climbing/Components/ClimbingComponent.cs b/Content.Server/Climbing/Components/ClimbingComponent.cs index 6c9a45fa7288c1..b6c0bedc9d9200 100644 --- a/Content.Server/Climbing/Components/ClimbingComponent.cs +++ b/Content.Server/Climbing/Components/ClimbingComponent.cs @@ -1,92 +1,34 @@ -using System; -using Content.Shared.Buckle.Components; using Content.Shared.Climbing; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Maths; -using Robust.Shared.Players; -using Robust.Shared.Timing; -namespace Content.Server.Climbing.Components +namespace Content.Server.Climbing.Components; + +[RegisterComponent] +[ComponentReference(typeof(SharedClimbingComponent))] +public sealed class ClimbingComponent : SharedClimbingComponent { - [RegisterComponent] - [ComponentReference(typeof(SharedClimbingComponent))] - public sealed class ClimbingComponent : SharedClimbingComponent + [ViewVariables(VVAccess.ReadWrite)] + public override bool IsClimbing { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - - public override bool IsClimbing - { - get => base.IsClimbing; - set - { - if (base.IsClimbing == value) - return; - - base.IsClimbing = value; - - if (value) - { - StartClimbTime = IoCManager.Resolve().CurTime; - EntitySystem.Get().AddActiveClimber(this); - OwnerIsTransitioning = true; - } - else - { - EntitySystem.Get().RemoveActiveClimber(this); - OwnerIsTransitioning = false; - } - - Dirty(); - } - } - - public override bool OwnerIsTransitioning - { - get => base.OwnerIsTransitioning; - set - { - if (value == base.OwnerIsTransitioning) return; - base.OwnerIsTransitioning = value; - Dirty(); - } - } - - /// - /// Make the owner climb from one point to another - /// - public void TryMoveTo(Vector2 from, Vector2 to) + get => base.IsClimbing; + set { - if (!_entityManager.TryGetComponent(Owner, out var physicsComponent)) return; - - var velocity = (to - from).Length; - - if (velocity <= 0.0f) return; - - // Since there are bodies with different masses: - // mass * 5 seems enough to move entity - // instead of launching cats like rockets against the walls with constant impulse value. - physicsComponent.ApplyLinearImpulse((to - from).Normalized * velocity * physicsComponent.Mass * 5); - OwnerIsTransitioning = true; - - EntitySystem.Get().UnsetTransitionBoolAfterBufferTime(Owner, this); - } - - public void Update() - { - if (!IsClimbing || _gameTiming.CurTime < TimeSpan.FromSeconds(BufferTime) + StartClimbTime) - { - return; - } - - if (!IsOnClimbableThisFrame && IsClimbing) - IsClimbing = false; + if (base.IsClimbing == value) return; + base.IsClimbing = value; + Dirty(); } + } - public override ComponentState GetComponentState() + public override bool OwnerIsTransitioning + { + get => base.OwnerIsTransitioning; + set { - return new ClimbModeComponentState(base.IsClimbing, OwnerIsTransitioning); + if (value == base.OwnerIsTransitioning) return; + base.OwnerIsTransitioning = value; + Dirty(); } } + + [ViewVariables] + public Dictionary DisabledFixtureMasks { get; } = new(); } diff --git a/Content.Shared/Climbing/SharedClimbSystem.cs b/Content.Shared/Climbing/SharedClimbSystem.cs index 0af9e05d902fe5..a230642cbeea19 100644 --- a/Content.Shared/Climbing/SharedClimbSystem.cs +++ b/Content.Shared/Climbing/SharedClimbSystem.cs @@ -1,23 +1,28 @@ +using Content.Shared.DragDrop; using Content.Shared.Movement; -using Robust.Shared.GameObjects; -namespace Content.Shared.Climbing +namespace Content.Shared.Climbing; + +public abstract class SharedClimbSystem : EntitySystem { - public abstract class SharedClimbSystem : EntitySystem + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(HandleMoveAttempt); + SubscribeLocalEvent(OnCanDragDropOn); + } + + private static void HandleMoveAttempt(EntityUid uid, SharedClimbingComponent component, UpdateCanMoveEvent args) { - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(HandleMoveAttempt); - } + if (component.LifeStage > ComponentLifeStage.Running) + return; - private void HandleMoveAttempt(EntityUid uid, SharedClimbingComponent component, UpdateCanMoveEvent args) - { - if (component.LifeStage > ComponentLifeStage.Running) - return; + if (component.OwnerIsTransitioning) + args.Cancel(); + } - if (component.OwnerIsTransitioning) - args.Cancel(); - } + protected virtual void OnCanDragDropOn(EntityUid uid, SharedClimbableComponent component, CanDragDropOnEvent args) + { + args.CanDrop = HasComp(args.Dragged); } } diff --git a/Content.Shared/Climbing/SharedClimbableComponent.cs b/Content.Shared/Climbing/SharedClimbableComponent.cs index da6266a113025f..61907d6c65d015 100644 --- a/Content.Shared/Climbing/SharedClimbableComponent.cs +++ b/Content.Shared/Climbing/SharedClimbableComponent.cs @@ -1,26 +1,13 @@ using Content.Shared.DragDrop; using Content.Shared.Interaction; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.ViewVariables; namespace Content.Shared.Climbing { - public interface IClimbable { } - - public abstract class SharedClimbableComponent : Component, IClimbable, IDragDropOn + public abstract class SharedClimbableComponent : Component { /// /// The range from which this entity can be climbed. /// - [ViewVariables] [DataField("range")] protected float Range = SharedInteractionSystem.InteractionRange / 1.4f; - - public virtual bool CanDragDropOn(DragDropEvent eventArgs) - { - return IoCManager.Resolve().HasComponent(eventArgs.Dragged); - } - - public abstract bool DragDropOn(DragDropEvent eventArgs); + [ViewVariables] [DataField("range")] public float Range = SharedInteractionSystem.InteractionRange / 1.4f; } } diff --git a/Content.Shared/Climbing/SharedClimbingComponent.cs b/Content.Shared/Climbing/SharedClimbingComponent.cs index d3eafb3919ef17..23db85df7303b0 100644 --- a/Content.Shared/Climbing/SharedClimbingComponent.cs +++ b/Content.Shared/Climbing/SharedClimbingComponent.cs @@ -1,122 +1,38 @@ -using Content.Shared.ActionBlocker; -using Content.Shared.Physics; using Robust.Shared.GameStates; -using Robust.Shared.Physics; using Robust.Shared.Serialization; -namespace Content.Shared.Climbing +namespace Content.Shared.Climbing; + +[NetworkedComponent] +public abstract class SharedClimbingComponent : Component { - [NetworkedComponent()] - public abstract class SharedClimbingComponent : Component + /// + /// Whether the owner is climbing on a climbable entity. + /// + [ViewVariables] + public virtual bool IsClimbing { get; set; } + + /// + /// Whether the owner is being moved onto the climbed entity. + /// + [ViewVariables] + public virtual bool OwnerIsTransitioning { get; set; } + + /// + /// We'll launch the mob onto the table and give them at least this amount of time to be on it. + /// + public const float BufferTime = 0.3f; + + [Serializable, NetSerializable] + public sealed class ClimbModeComponentState : ComponentState { - [Dependency] private readonly IEntityManager _entMan = default!; - [Dependency] private readonly IEntitySystemManager _sysMan = default!; - - /// - /// List of fixtures that had vault-impassable prior to an entity being downed. Required when re-adding the - /// collision mask. - /// - [DataField("vaultImpassableFixtures")] - public List VaultImpassableFixtures = new(); - - protected bool IsOnClimbableThisFrame - { - get - { - if (!_entMan.TryGetComponent(Owner, out var physicsComponent)) return false; - - foreach (var entity in physicsComponent.GetBodiesIntersecting()) - { - if ((entity.CollisionLayer & (int) CollisionGroup.VaultImpassable) != 0) return true; - } - - return false; - } - } - - [ViewVariables] - public virtual bool OwnerIsTransitioning - { - get => _ownerIsTransitioning; - set - { - if (_ownerIsTransitioning == value) return; - _ownerIsTransitioning = value; - if (!_entMan.TryGetComponent(Owner, out var physicsComponent)) return; - if (value) - { - physicsComponent.BodyType = BodyType.Dynamic; - } - else - { - physicsComponent.BodyType = BodyType.KinematicController; - } - - _sysMan.GetEntitySystem().UpdateCanMove(Owner); - } - } - - private bool _ownerIsTransitioning = false; - - protected TimeSpan StartClimbTime = TimeSpan.Zero; - - /// - /// We'll launch the mob onto the table and give them at least this amount of time to be on it. - /// - public const float BufferTime = 0.3f; - - public virtual bool IsClimbing + public ClimbModeComponentState(bool climbing, bool isTransitioning) { - get => _isClimbing; - set - { - if (_isClimbing == value) return; - _isClimbing = value; - - ToggleSmallPassable(value); - } + Climbing = climbing; + IsTransitioning = isTransitioning; } - private bool _isClimbing; - - // TODO: Layers need a re-work - private void ToggleSmallPassable(bool value) - { - // Hope the mob has one fixture - if (!_entMan.TryGetComponent(Owner, out var fixturesComponent) || fixturesComponent.Deleted) return; - - if (value) - { - foreach (var (key, fixture) in fixturesComponent.Fixtures) - { - if ((fixture.CollisionMask & (int) CollisionGroup.VaultImpassable) == 0) - continue; - - VaultImpassableFixtures.Add(key); - fixture.CollisionMask &= ~(int) CollisionGroup.VaultImpassable; - } - return; - } - - foreach (var key in VaultImpassableFixtures) - { - if (fixturesComponent.Fixtures.TryGetValue(key, out var fixture)) - fixture.CollisionMask |= (int) CollisionGroup.VaultImpassable; - } - VaultImpassableFixtures.Clear(); - } - - [Serializable, NetSerializable] - protected sealed class ClimbModeComponentState : ComponentState - { - public ClimbModeComponentState(bool climbing, bool isTransitioning) - { - Climbing = climbing; - IsTransitioning = isTransitioning; - } - - public bool Climbing { get; } - public bool IsTransitioning { get; } - } + public bool Climbing { get; } + public bool IsTransitioning { get; } } }