ClimbSystem.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. using Content.Shared.ActionBlocker;
  2. using Content.Shared.Buckle.Components;
  3. using Content.Shared.Climbing.Components;
  4. using Content.Shared.Climbing.Events;
  5. using Content.Shared.Damage;
  6. using Content.Shared.DoAfter;
  7. using Content.Shared.DragDrop;
  8. using Content.Shared.Hands.Components;
  9. using Content.Shared.IdentityManagement;
  10. using Content.Shared.Interaction;
  11. using Content.Shared.Movement.Events;
  12. using Content.Shared.Physics;
  13. using Content.Shared.Popups;
  14. using Content.Shared.Stunnable;
  15. using Content.Shared.Verbs;
  16. using Robust.Shared.Audio.Systems;
  17. using Robust.Shared.Containers;
  18. using Robust.Shared.Physics;
  19. using Robust.Shared.Physics.Collision.Shapes;
  20. using Robust.Shared.Physics.Components;
  21. using Robust.Shared.Physics.Controllers;
  22. using Robust.Shared.Physics.Events;
  23. using Robust.Shared.Physics.Systems;
  24. using Robust.Shared.Player;
  25. using Robust.Shared.Serialization;
  26. using Robust.Shared.Timing;
  27. namespace Content.Shared.Climbing.Systems;
  28. public sealed partial class ClimbSystem : VirtualController
  29. {
  30. [Dependency] private readonly IGameTiming _timing = default!;
  31. [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
  32. [Dependency] private readonly DamageableSystem _damageableSystem = default!;
  33. [Dependency] private readonly FixtureSystem _fixtureSystem = default!;
  34. [Dependency] private readonly SharedAudioSystem _audio = default!;
  35. [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
  36. [Dependency] private readonly SharedContainerSystem _containers = default!;
  37. [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
  38. [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
  39. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  40. [Dependency] private readonly SharedStunSystem _stunSystem = default!;
  41. [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
  42. private const string ClimbingFixtureName = "climb";
  43. private const int ClimbingCollisionGroup = (int) (CollisionGroup.TableLayer | CollisionGroup.LowImpassable);
  44. private EntityQuery<ClimbableComponent> _climbableQuery;
  45. private EntityQuery<FixturesComponent> _fixturesQuery;
  46. private EntityQuery<TransformComponent> _xformQuery;
  47. public override void Initialize()
  48. {
  49. base.Initialize();
  50. _climbableQuery = GetEntityQuery<ClimbableComponent>();
  51. _fixturesQuery = GetEntityQuery<FixturesComponent>();
  52. _xformQuery = GetEntityQuery<TransformComponent>();
  53. SubscribeLocalEvent<ClimbingComponent, UpdateCanMoveEvent>(OnMoveAttempt);
  54. SubscribeLocalEvent<ClimbingComponent, EntParentChangedMessage>(OnParentChange);
  55. SubscribeLocalEvent<ClimbingComponent, ClimbDoAfterEvent>(OnDoAfter);
  56. SubscribeLocalEvent<ClimbingComponent, EndCollideEvent>(OnClimbEndCollide);
  57. SubscribeLocalEvent<ClimbingComponent, BuckledEvent>(OnBuckled);
  58. SubscribeLocalEvent<ClimbableComponent, CanDropTargetEvent>(OnCanDragDropOn);
  59. SubscribeLocalEvent<ClimbableComponent, GetVerbsEvent<AlternativeVerb>>(AddClimbableVerb);
  60. SubscribeLocalEvent<ClimbableComponent, DragDropTargetEvent>(OnClimbableDragDrop);
  61. SubscribeLocalEvent<GlassTableComponent, ClimbedOnEvent>(OnGlassClimbed);
  62. }
  63. public override void UpdateBeforeSolve(bool prediction, float frameTime)
  64. {
  65. base.UpdateBeforeSolve(prediction, frameTime);
  66. var query = EntityQueryEnumerator<ClimbingComponent>();
  67. var curTime = _timing.CurTime;
  68. // Move anything still climb in the specified direction.
  69. while (query.MoveNext(out var uid, out var comp))
  70. {
  71. if (comp.NextTransition == null)
  72. continue;
  73. if (comp.NextTransition < curTime)
  74. {
  75. FinishTransition(uid, comp);
  76. continue;
  77. }
  78. var xform = _xformQuery.GetComponent(uid);
  79. _xformSystem.SetLocalPosition(uid, xform.LocalPosition + comp.Direction * frameTime, xform);
  80. }
  81. }
  82. private void FinishTransition(EntityUid uid, ClimbingComponent comp)
  83. {
  84. // TODO: Validate climb here
  85. comp.NextTransition = null;
  86. _actionBlockerSystem.UpdateCanMove(uid);
  87. Dirty(uid, comp);
  88. // Stop if necessary.
  89. if (!_fixturesQuery.TryGetComponent(uid, out var fixtures) ||
  90. !IsClimbing(uid, fixtures))
  91. {
  92. StopClimb(uid, comp);
  93. return;
  94. }
  95. }
  96. /// <summary>
  97. /// Returns true if entity currently has a valid vault.
  98. /// </summary>
  99. private bool IsClimbing(EntityUid uid, FixturesComponent? fixturesComp = null)
  100. {
  101. if (!_fixturesQuery.Resolve(uid, ref fixturesComp) || !fixturesComp.Fixtures.TryGetValue(ClimbingFixtureName, out var climbFixture))
  102. return false;
  103. foreach (var contact in climbFixture.Contacts.Values)
  104. {
  105. var other = uid == contact.EntityA ? contact.EntityB : contact.EntityA;
  106. if (HasComp<ClimbableComponent>(other))
  107. {
  108. return true;
  109. }
  110. }
  111. return false;
  112. }
  113. private void OnMoveAttempt(EntityUid uid, ClimbingComponent component, UpdateCanMoveEvent args)
  114. {
  115. // Can't move when transition.
  116. if (component.NextTransition != null)
  117. args.Cancel();
  118. }
  119. private void OnParentChange(EntityUid uid, ClimbingComponent component, ref EntParentChangedMessage args)
  120. {
  121. if (component.NextTransition != null)
  122. {
  123. FinishTransition(uid, component);
  124. }
  125. }
  126. private void OnCanDragDropOn(EntityUid uid, ClimbableComponent component, ref CanDropTargetEvent args)
  127. {
  128. if (args.Handled)
  129. return;
  130. // If already climbing then don't show outlines.
  131. if (TryComp(args.Dragged, out ClimbingComponent? climbing) && climbing.IsClimbing)
  132. return;
  133. var canVault = args.User == args.Dragged
  134. ? CanVault(component, args.User, uid, out _)
  135. : CanVault(component, args.User, args.Dragged, uid, out _);
  136. args.CanDrop = canVault;
  137. if (!HasComp<HandsComponent>(args.User))
  138. args.CanDrop = false;
  139. args.Handled = true;
  140. }
  141. private void AddClimbableVerb(EntityUid uid, ClimbableComponent component, GetVerbsEvent<AlternativeVerb> args)
  142. {
  143. if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanMove(args.User))
  144. return;
  145. if (!TryComp(args.User, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing || !climbingComponent.CanClimb)
  146. return;
  147. // TODO VERBS ICON add a climbing icon?
  148. args.Verbs.Add(new AlternativeVerb
  149. {
  150. Act = () => TryClimb(args.User, args.User, args.Target, out _, component),
  151. Text = Loc.GetString("comp-climbable-verb-climb")
  152. });
  153. }
  154. private void OnClimbableDragDrop(EntityUid uid, ClimbableComponent component, ref DragDropTargetEvent args)
  155. {
  156. if (args.Handled)
  157. return;
  158. TryClimb(args.User, args.Dragged, uid, out _, component);
  159. }
  160. public bool TryClimb(
  161. EntityUid user,
  162. EntityUid entityToMove,
  163. EntityUid climbable,
  164. out DoAfterId? id,
  165. ClimbableComponent? comp = null,
  166. ClimbingComponent? climbing = null)
  167. {
  168. id = null;
  169. if (!Resolve(climbable, ref comp) || !Resolve(entityToMove, ref climbing, false))
  170. return false;
  171. var canVault = user == entityToMove
  172. ? CanVault(comp, user, climbable, out var reason)
  173. : CanVault(comp, user, entityToMove, climbable, out reason);
  174. if (!canVault)
  175. {
  176. _popupSystem.PopupClient(reason, user, user);
  177. return false;
  178. }
  179. // Note, IsClimbing does not mean a DoAfter is active, it means the target has already finished a DoAfter and
  180. // is currently on top of something..
  181. if (climbing.IsClimbing)
  182. return true;
  183. var ev = new AttemptClimbEvent(user, entityToMove, climbable);
  184. RaiseLocalEvent(climbable, ref ev);
  185. if (ev.Cancelled)
  186. return false;
  187. var args = new DoAfterArgs(EntityManager, user, comp.ClimbDelay, new ClimbDoAfterEvent(),
  188. entityToMove,
  189. target: climbable,
  190. used: entityToMove)
  191. {
  192. BreakOnMove = true,
  193. BreakOnDamage = true,
  194. DuplicateCondition = DuplicateConditions.SameTool | DuplicateConditions.SameTarget
  195. };
  196. _audio.PlayPredicted(comp.StartClimbSound, climbable, user);
  197. return _doAfterSystem.TryStartDoAfter(args, out id);
  198. }
  199. private void OnDoAfter(EntityUid uid, ClimbingComponent component, ClimbDoAfterEvent args)
  200. {
  201. if (args.Handled || args.Cancelled || args.Args.Target == null || args.Args.Used == null)
  202. return;
  203. Climb(uid, args.Args.User, args.Args.Target.Value, climbing: component);
  204. args.Handled = true;
  205. }
  206. private void Climb(EntityUid uid, EntityUid user, EntityUid climbable, bool silent = false, ClimbingComponent? climbing = null,
  207. PhysicsComponent? physics = null, FixturesComponent? fixtures = null, ClimbableComponent? comp = null)
  208. {
  209. if (!Resolve(uid, ref climbing, ref physics, ref fixtures, false))
  210. return;
  211. if (!Resolve(climbable, ref comp, false))
  212. return;
  213. var selfEvent = new SelfBeforeClimbEvent(uid, user, (climbable, comp));
  214. RaiseLocalEvent(uid, selfEvent);
  215. if (selfEvent.Cancelled)
  216. return;
  217. var targetEvent = new TargetBeforeClimbEvent(uid, user, (climbable, comp));
  218. RaiseLocalEvent(climbable, targetEvent);
  219. if (targetEvent.Cancelled)
  220. return;
  221. if (!ReplaceFixtures(uid, climbing, fixtures))
  222. return;
  223. var xform = _xformQuery.GetComponent(uid);
  224. var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
  225. var worldDirection = _xformSystem.GetWorldPosition(climbable) - worldPos;
  226. var distance = worldDirection.Length();
  227. var parentRot = worldRot - xform.LocalRotation;
  228. // Need direction relative to climber's parent.
  229. var localDirection = (-parentRot).RotateVec(worldDirection);
  230. // On top of it already so just do it in place.
  231. if (localDirection.LengthSquared() < 0.01f)
  232. {
  233. climbing.NextTransition = null;
  234. }
  235. // VirtualController over to the thing.
  236. else
  237. {
  238. var climbDuration = TimeSpan.FromSeconds(distance / climbing.TransitionRate);
  239. climbing.NextTransition = _timing.CurTime + climbDuration;
  240. climbing.Direction = localDirection.Normalized() * climbing.TransitionRate;
  241. _actionBlockerSystem.UpdateCanMove(uid);
  242. }
  243. climbing.IsClimbing = true;
  244. Dirty(uid, climbing);
  245. _audio.PlayPredicted(comp.FinishClimbSound, climbable, user);
  246. var startEv = new StartClimbEvent(climbable);
  247. var climbedEv = new ClimbedOnEvent(uid, user);
  248. RaiseLocalEvent(uid, ref startEv);
  249. RaiseLocalEvent(climbable, ref climbedEv);
  250. if (silent)
  251. return;
  252. string selfMessage;
  253. string othersMessage;
  254. if (user == uid)
  255. {
  256. othersMessage = Loc.GetString("comp-climbable-user-climbs-other",
  257. ("user", Identity.Entity(uid, EntityManager)),
  258. ("climbable", climbable));
  259. selfMessage = Loc.GetString("comp-climbable-user-climbs", ("climbable", climbable));
  260. }
  261. else
  262. {
  263. othersMessage = Loc.GetString("comp-climbable-user-climbs-force-other",
  264. ("user", Identity.Entity(user, EntityManager)),
  265. ("moved-user", Identity.Entity(uid, EntityManager)), ("climbable", climbable));
  266. selfMessage = Loc.GetString("comp-climbable-user-climbs-force", ("moved-user", Identity.Entity(uid, EntityManager)),
  267. ("climbable", climbable));
  268. }
  269. _popupSystem.PopupPredicted(selfMessage, othersMessage, uid, user);
  270. }
  271. /// <summary>
  272. /// Replaces the current fixtures with non-climbing collidable versions so that climb end can be detected
  273. /// </summary>
  274. /// <returns>Returns whether adding the new fixtures was successful</returns>
  275. private bool ReplaceFixtures(EntityUid uid, ClimbingComponent climbingComp, FixturesComponent fixturesComp)
  276. {
  277. // Swap fixtures
  278. foreach (var (name, fixture) in fixturesComp.Fixtures)
  279. {
  280. if (climbingComp.DisabledFixtureMasks.ContainsKey(name)
  281. || fixture.Hard == false
  282. || (fixture.CollisionMask & ClimbingCollisionGroup) == 0)
  283. {
  284. continue;
  285. }
  286. climbingComp.DisabledFixtureMasks.Add(name, fixture.CollisionMask & ClimbingCollisionGroup);
  287. _physics.SetCollisionMask(uid, name, fixture, fixture.CollisionMask & ~ClimbingCollisionGroup, fixturesComp);
  288. }
  289. if (!_fixtureSystem.TryCreateFixture(
  290. uid,
  291. new PhysShapeCircle(0.35f),
  292. ClimbingFixtureName,
  293. collisionLayer: (int) CollisionGroup.None,
  294. collisionMask: ClimbingCollisionGroup,
  295. hard: false,
  296. manager: fixturesComp))
  297. {
  298. return false;
  299. }
  300. return true;
  301. }
  302. private void OnClimbEndCollide(EntityUid uid, ClimbingComponent component, ref EndCollideEvent args)
  303. {
  304. if (args.OurFixtureId != ClimbingFixtureName
  305. || !component.IsClimbing
  306. || component.NextTransition != null)
  307. {
  308. return;
  309. }
  310. foreach (var contact in args.OurFixture.Contacts.Values)
  311. {
  312. if (!contact.IsTouching)
  313. continue;
  314. var otherEnt = contact.OtherEnt(uid);
  315. var (otherFixtureId, otherFixture) = contact.OtherFixture(uid);
  316. // TODO: Remove this on engine.
  317. if (args.OtherEntity == otherEnt && args.OtherFixtureId == otherFixtureId)
  318. continue;
  319. if (otherFixture is { Hard: true } &&
  320. _climbableQuery.HasComp(otherEnt))
  321. {
  322. return;
  323. }
  324. }
  325. // TODO: Is this even needed anymore?
  326. foreach (var otherFixture in args.OurFixture.Contacts.Keys)
  327. {
  328. // If it's the other fixture then ignore em
  329. if (otherFixture == args.OtherFixture)
  330. continue;
  331. // If still colliding with a climbable, do not stop climbing
  332. if (HasComp<ClimbableComponent>(otherFixture.Owner))
  333. return;
  334. }
  335. StopClimb(uid, component);
  336. }
  337. private void StopClimb(EntityUid uid, ClimbingComponent? climbing = null, FixturesComponent? fixtures = null)
  338. {
  339. if (!Resolve(uid, ref climbing, ref fixtures, false))
  340. return;
  341. foreach (var (name, fixtureMask) in climbing.DisabledFixtureMasks)
  342. {
  343. if (!fixtures.Fixtures.TryGetValue(name, out var fixture))
  344. {
  345. continue;
  346. }
  347. _physics.SetCollisionMask(uid, name, fixture, fixture.CollisionMask | fixtureMask, fixtures);
  348. }
  349. climbing.DisabledFixtureMasks.Clear();
  350. _fixtureSystem.DestroyFixture(uid, ClimbingFixtureName, manager: fixtures);
  351. climbing.IsClimbing = false;
  352. climbing.NextTransition = null;
  353. var ev = new EndClimbEvent();
  354. RaiseLocalEvent(uid, ref ev);
  355. Dirty(uid, climbing);
  356. }
  357. /// <summary>
  358. /// Checks if the user can vault the target
  359. /// </summary>
  360. /// <param name="component">The component of the entity that is being vaulted</param>
  361. /// <param name="user">The entity that wants to vault</param>
  362. /// <param name="target">The object that is being vaulted</param>
  363. /// <param name="reason">The reason why it cant be dropped</param>
  364. public bool CanVault(ClimbableComponent component, EntityUid user, EntityUid target, out string reason)
  365. {
  366. if (!_actionBlockerSystem.CanInteract(user, target))
  367. {
  368. reason = Loc.GetString("comp-climbable-cant-interact");
  369. return false;
  370. }
  371. if (!TryComp<ClimbingComponent>(user, out var climbingComp)
  372. || !climbingComp.CanClimb)
  373. {
  374. reason = Loc.GetString("comp-climbable-cant-climb");
  375. return false;
  376. }
  377. if (!_interactionSystem.InRangeUnobstructed(user, target, component.Range))
  378. {
  379. reason = Loc.GetString("comp-climbable-cant-reach");
  380. return false;
  381. }
  382. if (_containers.IsEntityInContainer(user))
  383. {
  384. reason = Loc.GetString("comp-climbable-cant-reach");
  385. return false;
  386. }
  387. reason = string.Empty;
  388. return true;
  389. }
  390. /// <summary>
  391. /// Checks if the user can vault the dragged entity onto the the target
  392. /// </summary>
  393. /// <param name="component">The climbable component of the object being vaulted onto</param>
  394. /// <param name="user">The user that wants to vault the entity</param>
  395. /// <param name="dragged">The entity that is being vaulted</param>
  396. /// <param name="target">The object that is being vaulted onto</param>
  397. /// <param name="reason">The reason why it cant be dropped</param>
  398. /// <returns></returns>
  399. public bool CanVault(ClimbableComponent component, EntityUid user, EntityUid dragged, EntityUid target,
  400. out string reason)
  401. {
  402. if (!_actionBlockerSystem.CanInteract(user, dragged) || !_actionBlockerSystem.CanInteract(user, target))
  403. {
  404. reason = Loc.GetString("comp-climbable-cant-interact");
  405. return false;
  406. }
  407. if (!HasComp<ClimbingComponent>(dragged))
  408. {
  409. reason = Loc.GetString("comp-climbable-target-cant-climb", ("moved-user", Identity.Entity(dragged, EntityManager)));
  410. return false;
  411. }
  412. bool Ignored(EntityUid entity) => entity == target || entity == user || entity == dragged;
  413. if (!_interactionSystem.InRangeUnobstructed(user, target, component.Range, predicate: Ignored)
  414. || !_interactionSystem.InRangeUnobstructed(user, dragged, component.Range, predicate: Ignored))
  415. {
  416. reason = Loc.GetString("comp-climbable-cant-reach");
  417. return false;
  418. }
  419. if (_containers.IsEntityInContainer(user) || _containers.IsEntityInContainer(dragged))
  420. {
  421. reason = Loc.GetString("comp-climbable-cant-reach");
  422. return false;
  423. }
  424. reason = string.Empty;
  425. return true;
  426. }
  427. public void ForciblySetClimbing(EntityUid uid, EntityUid climbable, ClimbingComponent? component = null)
  428. {
  429. Climb(uid, uid, climbable, true, component);
  430. }
  431. private void OnBuckled(EntityUid uid, ClimbingComponent component, ref BuckledEvent args)
  432. {
  433. StopClimb(uid, component);
  434. }
  435. private void OnGlassClimbed(EntityUid uid, GlassTableComponent component, ref ClimbedOnEvent args)
  436. {
  437. if (TryComp<PhysicsComponent>(args.Climber, out var physics) && physics.Mass <= component.MassLimit)
  438. return;
  439. _damageableSystem.TryChangeDamage(args.Climber, component.ClimberDamage, origin: args.Climber);
  440. _damageableSystem.TryChangeDamage(uid, component.TableDamage, origin: args.Climber);
  441. _stunSystem.TryParalyze(args.Climber, TimeSpan.FromSeconds(component.StunTime), true);
  442. // Not shown to the user, since they already get a 'you climb on the glass table' popup
  443. _popupSystem.PopupEntity(
  444. Loc.GetString("glass-table-shattered-others", ("table", uid), ("climber", Identity.Entity(args.Climber, EntityManager))), args.Climber,
  445. Filter.PvsExcept(args.Climber), true);
  446. }
  447. [Serializable, NetSerializable]
  448. private sealed partial class ClimbDoAfterEvent : SimpleDoAfterEvent
  449. {
  450. }
  451. }