SharedMagicSystem.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. using System.Numerics;
  2. using Content.Shared.Actions;
  3. using Content.Shared.Body.Components;
  4. using Content.Shared.Body.Systems;
  5. using Content.Shared.Coordinates.Helpers;
  6. using Content.Shared.Doors.Components;
  7. using Content.Shared.Doors.Systems;
  8. using Content.Shared.Hands.Components;
  9. using Content.Shared.Hands.EntitySystems;
  10. using Content.Shared.Interaction;
  11. using Content.Shared.Inventory;
  12. using Content.Shared.Lock;
  13. using Content.Shared.Magic.Components;
  14. using Content.Shared.Magic.Events;
  15. using Content.Shared.Maps;
  16. using Content.Shared.Mind;
  17. using Content.Shared.Physics;
  18. using Content.Shared.Popups;
  19. using Content.Shared.Speech.Muting;
  20. using Content.Shared.Storage;
  21. using Content.Shared.Stunnable;
  22. using Content.Shared.Tag;
  23. using Content.Shared.Weapons.Ranged.Components;
  24. using Content.Shared.Weapons.Ranged.Systems;
  25. using Robust.Shared.Audio.Systems;
  26. using Robust.Shared.Map;
  27. using Robust.Shared.Map.Components;
  28. using Robust.Shared.Network;
  29. using Robust.Shared.Physics.Systems;
  30. using Robust.Shared.Prototypes;
  31. using Robust.Shared.Random;
  32. using Robust.Shared.Serialization.Manager;
  33. using Robust.Shared.Spawners;
  34. using Content.Shared._Shitmed.Targeting;
  35. namespace Content.Shared.Magic;
  36. // TODO: Move BeforeCast & Prerequirements (like Wizard clothes) to action comp
  37. // Alt idea - make it its own comp and split, like the Charge PR
  38. // TODO: Move speech to actionComp or again, its own ECS
  39. // TODO: Use the MagicComp just for pure backend things like spawning patterns?
  40. /// <summary>
  41. /// Handles learning and using spells (actions)
  42. /// </summary>
  43. public abstract class SharedMagicSystem : EntitySystem
  44. {
  45. [Dependency] private readonly ISerializationManager _seriMan = default!;
  46. [Dependency] private readonly IComponentFactory _compFact = default!;
  47. [Dependency] private readonly IMapManager _mapManager = default!;
  48. [Dependency] private readonly SharedMapSystem _mapSystem = default!;
  49. [Dependency] private readonly IRobustRandom _random = default!;
  50. [Dependency] private readonly SharedGunSystem _gunSystem = default!;
  51. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  52. [Dependency] private readonly SharedTransformSystem _transform = default!;
  53. [Dependency] private readonly INetManager _net = default!;
  54. [Dependency] private readonly SharedBodySystem _body = default!;
  55. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  56. [Dependency] private readonly SharedDoorSystem _door = default!;
  57. [Dependency] private readonly InventorySystem _inventory = default!;
  58. [Dependency] private readonly SharedPopupSystem _popup = default!;
  59. [Dependency] private readonly SharedInteractionSystem _interaction = default!;
  60. [Dependency] private readonly LockSystem _lock = default!;
  61. [Dependency] private readonly SharedHandsSystem _hands = default!;
  62. [Dependency] private readonly TagSystem _tag = default!;
  63. [Dependency] private readonly SharedAudioSystem _audio = default!;
  64. [Dependency] private readonly SharedMindSystem _mind = default!;
  65. [Dependency] private readonly SharedStunSystem _stun = default!;
  66. public override void Initialize()
  67. {
  68. base.Initialize();
  69. SubscribeLocalEvent<MagicComponent, BeforeCastSpellEvent>(OnBeforeCastSpell);
  70. SubscribeLocalEvent<InstantSpawnSpellEvent>(OnInstantSpawn);
  71. SubscribeLocalEvent<TeleportSpellEvent>(OnTeleportSpell);
  72. SubscribeLocalEvent<WorldSpawnSpellEvent>(OnWorldSpawn);
  73. SubscribeLocalEvent<ProjectileSpellEvent>(OnProjectileSpell);
  74. SubscribeLocalEvent<ChangeComponentsSpellEvent>(OnChangeComponentsSpell);
  75. SubscribeLocalEvent<SmiteSpellEvent>(OnSmiteSpell);
  76. SubscribeLocalEvent<KnockSpellEvent>(OnKnockSpell);
  77. SubscribeLocalEvent<ChargeSpellEvent>(OnChargeSpell);
  78. SubscribeLocalEvent<RandomGlobalSpawnSpellEvent>(OnRandomGlobalSpawnSpell);
  79. SubscribeLocalEvent<MindSwapSpellEvent>(OnMindSwapSpell);
  80. SubscribeLocalEvent<VoidApplauseSpellEvent>(OnVoidApplause);
  81. }
  82. private void OnBeforeCastSpell(Entity<MagicComponent> ent, ref BeforeCastSpellEvent args)
  83. {
  84. var comp = ent.Comp;
  85. var hasReqs = true;
  86. if (comp.RequiresClothes)
  87. {
  88. var enumerator = _inventory.GetSlotEnumerator(args.Performer, SlotFlags.OUTERCLOTHING | SlotFlags.HEAD);
  89. while (enumerator.MoveNext(out var containerSlot))
  90. {
  91. if (containerSlot.ContainedEntity is { } item)
  92. hasReqs = HasComp<WizardClothesComponent>(item);
  93. else
  94. hasReqs = false;
  95. if (!hasReqs)
  96. break;
  97. }
  98. }
  99. if (comp.RequiresSpeech && HasComp<MutedComponent>(args.Performer))
  100. hasReqs = false;
  101. if (hasReqs)
  102. return;
  103. args.Cancelled = true;
  104. _popup.PopupClient(Loc.GetString("spell-requirements-failed"), args.Performer, args.Performer);
  105. // TODO: Pre-cast do after, either here or in SharedActionsSystem
  106. }
  107. private bool PassesSpellPrerequisites(EntityUid spell, EntityUid performer)
  108. {
  109. var ev = new BeforeCastSpellEvent(performer);
  110. RaiseLocalEvent(spell, ref ev);
  111. return !ev.Cancelled;
  112. }
  113. #region Spells
  114. #region Instant Spawn Spells
  115. /// <summary>
  116. /// Handles the instant action (i.e. on the caster) attempting to spawn an entity.
  117. /// </summary>
  118. private void OnInstantSpawn(InstantSpawnSpellEvent args)
  119. {
  120. if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
  121. return;
  122. var transform = Transform(args.Performer);
  123. foreach (var position in GetInstantSpawnPositions(transform, args.PosData))
  124. {
  125. SpawnSpellHelper(args.Prototype, position, args.Performer, preventCollide: args.PreventCollideWithCaster);
  126. }
  127. Speak(args);
  128. args.Handled = true;
  129. }
  130. /// <summary>
  131. /// Gets spawn positions listed on <see cref="InstantSpawnSpellEvent"/>
  132. /// </summary>
  133. /// <exception cref="ArgumentOutOfRangeException"></exception>
  134. private List<EntityCoordinates> GetInstantSpawnPositions(TransformComponent casterXform, MagicInstantSpawnData data)
  135. {
  136. switch (data)
  137. {
  138. case TargetCasterPos:
  139. return new List<EntityCoordinates>(1) { casterXform.Coordinates };
  140. case TargetInFrontSingle:
  141. {
  142. var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
  143. if (!TryComp<MapGridComponent>(casterXform.GridUid, out var mapGrid))
  144. return new List<EntityCoordinates>();
  145. if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
  146. return new List<EntityCoordinates>();
  147. var tileIndex = tileReference.Value.GridIndices;
  148. return new List<EntityCoordinates>(1) { _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex) };
  149. }
  150. case TargetInFront:
  151. {
  152. var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
  153. if (!TryComp<MapGridComponent>(casterXform.GridUid, out var mapGrid))
  154. return new List<EntityCoordinates>();
  155. if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
  156. return new List<EntityCoordinates>();
  157. var tileIndex = tileReference.Value.GridIndices;
  158. var coords = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex);
  159. EntityCoordinates coordsPlus;
  160. EntityCoordinates coordsMinus;
  161. var dir = casterXform.LocalRotation.GetCardinalDir();
  162. switch (dir)
  163. {
  164. case Direction.North:
  165. case Direction.South:
  166. {
  167. coordsPlus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (1, 0));
  168. coordsMinus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (-1, 0));
  169. return new List<EntityCoordinates>(3)
  170. {
  171. coords,
  172. coordsPlus,
  173. coordsMinus,
  174. };
  175. }
  176. case Direction.East:
  177. case Direction.West:
  178. {
  179. coordsPlus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (0, 1));
  180. coordsMinus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (0, -1));
  181. return new List<EntityCoordinates>(3)
  182. {
  183. coords,
  184. coordsPlus,
  185. coordsMinus,
  186. };
  187. }
  188. }
  189. return new List<EntityCoordinates>();
  190. }
  191. default:
  192. throw new ArgumentOutOfRangeException();
  193. }
  194. }
  195. // End Instant Spawn Spells
  196. #endregion
  197. #region World Spawn Spells
  198. /// <summary>
  199. /// Spawns entities from a list within range of click.
  200. /// </summary>
  201. /// <remarks>
  202. /// It will offset entities after the first entity based on the OffsetVector2.
  203. /// </remarks>
  204. /// <param name="args"> The Spawn Spell Event args.</param>
  205. private void OnWorldSpawn(WorldSpawnSpellEvent args)
  206. {
  207. if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
  208. return;
  209. var targetMapCoords = args.Target;
  210. WorldSpawnSpellHelper(args.Prototypes, targetMapCoords, args.Performer, args.Lifetime, args.Offset);
  211. Speak(args);
  212. args.Handled = true;
  213. }
  214. /// <summary>
  215. /// Loops through a supplied list of entity prototypes and spawns them
  216. /// </summary>
  217. /// <remarks>
  218. /// If an offset of 0, 0 is supplied then the entities will all spawn on the same tile.
  219. /// Any other offset will spawn entities starting from the source Map Coordinates and will increment the supplied
  220. /// offset
  221. /// </remarks>
  222. /// <param name="entityEntries"> The list of Entities to spawn in</param>
  223. /// <param name="entityCoords"> Map Coordinates where the entities will spawn</param>
  224. /// <param name="lifetime"> Check to see if the entities should self delete</param>
  225. /// <param name="offsetVector2"> A Vector2 offset that the entities will spawn in</param>
  226. private void WorldSpawnSpellHelper(List<EntitySpawnEntry> entityEntries, EntityCoordinates entityCoords, EntityUid performer, float? lifetime, Vector2 offsetVector2)
  227. {
  228. var getProtos = EntitySpawnCollection.GetSpawns(entityEntries, _random);
  229. var offsetCoords = entityCoords;
  230. foreach (var proto in getProtos)
  231. {
  232. SpawnSpellHelper(proto, offsetCoords, performer, lifetime);
  233. offsetCoords = offsetCoords.Offset(offsetVector2);
  234. }
  235. }
  236. // End World Spawn Spells
  237. #endregion
  238. #region Projectile Spells
  239. private void OnProjectileSpell(ProjectileSpellEvent ev)
  240. {
  241. if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || !_net.IsServer)
  242. return;
  243. ev.Handled = true;
  244. Speak(ev);
  245. var xform = Transform(ev.Performer);
  246. var fromCoords = xform.Coordinates;
  247. var toCoords = ev.Target;
  248. var userVelocity = _physics.GetMapLinearVelocity(ev.Performer);
  249. // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map.
  250. var fromMap = fromCoords.ToMap(EntityManager, _transform);
  251. var spawnCoords = _mapManager.TryFindGridAt(fromMap, out var gridUid, out _)
  252. ? fromCoords.WithEntityId(gridUid, EntityManager)
  253. : new(_mapManager.GetMapEntityId(fromMap.MapId), fromMap.Position);
  254. var ent = Spawn(ev.Prototype, spawnCoords);
  255. var direction = toCoords.ToMapPos(EntityManager, _transform) -
  256. spawnCoords.ToMapPos(EntityManager, _transform);
  257. _gunSystem.ShootProjectile(ent, direction, userVelocity, ev.Performer, ev.Performer);
  258. }
  259. // End Projectile Spells
  260. #endregion
  261. #region Change Component Spells
  262. // staves.yml ActionRGB light
  263. private void OnChangeComponentsSpell(ChangeComponentsSpellEvent ev)
  264. {
  265. if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
  266. return;
  267. ev.Handled = true;
  268. if (ev.DoSpeech)
  269. Speak(ev);
  270. RemoveComponents(ev.Target, ev.ToRemove);
  271. AddComponents(ev.Target, ev.ToAdd);
  272. }
  273. // End Change Component Spells
  274. #endregion
  275. #region Teleport Spells
  276. // TODO: Rename to teleport clicked spell?
  277. /// <summary>
  278. /// Teleports the user to the clicked location
  279. /// </summary>
  280. /// <param name="args"></param>
  281. private void OnTeleportSpell(TeleportSpellEvent args)
  282. {
  283. if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
  284. return;
  285. var transform = Transform(args.Performer);
  286. if (transform.MapID != _transform.GetMapId(args.Target) || !_interaction.InRangeUnobstructed(args.Performer, args.Target, range: 1000F, collisionMask: CollisionGroup.Opaque, popup: true))
  287. return;
  288. _transform.SetCoordinates(args.Performer, args.Target);
  289. _transform.AttachToGridOrMap(args.Performer, transform);
  290. Speak(args);
  291. args.Handled = true;
  292. }
  293. public virtual void OnVoidApplause(VoidApplauseSpellEvent ev)
  294. {
  295. if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
  296. return;
  297. ev.Handled = true;
  298. Speak(ev);
  299. _transform.SwapPositions(ev.Performer, ev.Target);
  300. }
  301. // End Teleport Spells
  302. #endregion
  303. #region Spell Helpers
  304. private void SpawnSpellHelper(string? proto, EntityCoordinates position, EntityUid performer, float? lifetime = null, bool preventCollide = false)
  305. {
  306. if (!_net.IsServer)
  307. return;
  308. var ent = Spawn(proto, position.SnapToGrid(EntityManager, _mapManager));
  309. if (lifetime != null)
  310. {
  311. var comp = EnsureComp<TimedDespawnComponent>(ent);
  312. comp.Lifetime = lifetime.Value;
  313. }
  314. if (preventCollide)
  315. {
  316. var comp = EnsureComp<PreventCollideComponent>(ent);
  317. comp.Uid = performer;
  318. }
  319. }
  320. private void AddComponents(EntityUid target, ComponentRegistry comps)
  321. {
  322. foreach (var (name, data) in comps)
  323. {
  324. if (HasComp(target, data.Component.GetType()))
  325. continue;
  326. var component = (Component)_compFact.GetComponent(name);
  327. var temp = (object)component;
  328. _seriMan.CopyTo(data.Component, ref temp);
  329. EntityManager.AddComponent(target, (Component)temp!);
  330. }
  331. }
  332. private void RemoveComponents(EntityUid target, HashSet<string> comps)
  333. {
  334. foreach (var toRemove in comps)
  335. {
  336. if (_compFact.TryGetRegistration(toRemove, out var registration))
  337. RemComp(target, registration.Type);
  338. }
  339. }
  340. // End Spell Helpers
  341. #endregion
  342. #region Touch Spells
  343. private void OnSmiteSpell(SmiteSpellEvent ev)
  344. {
  345. if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
  346. return;
  347. ev.Handled = true;
  348. Speak(ev);
  349. var direction = _transform.GetMapCoordinates(ev.Target, Transform(ev.Target)).Position - _transform.GetMapCoordinates(ev.Performer, Transform(ev.Performer)).Position;
  350. var impulseVector = direction * 10000;
  351. _physics.ApplyLinearImpulse(ev.Target, impulseVector);
  352. if (!TryComp<BodyComponent>(ev.Target, out var body))
  353. return;
  354. _body.GibBody(ev.Target, true, body);
  355. }
  356. // End Touch Spells
  357. #endregion
  358. #region Knock Spells
  359. /// <summary>
  360. /// Opens all doors and locks within range
  361. /// </summary>
  362. /// <param name="args"></param>
  363. private void OnKnockSpell(KnockSpellEvent args)
  364. {
  365. if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
  366. return;
  367. args.Handled = true;
  368. Speak(args);
  369. var transform = Transform(args.Performer);
  370. // Look for doors and lockers, and don't open/unlock them if they're already opened/unlocked.
  371. foreach (var target in _lookup.GetEntitiesInRange(_transform.GetMapCoordinates(args.Performer, transform), args.Range, flags: LookupFlags.Dynamic | LookupFlags.Static))
  372. {
  373. if (!_interaction.InRangeUnobstructed(args.Performer, target, range: 0, collisionMask: CollisionGroup.Opaque))
  374. continue;
  375. if (TryComp<DoorBoltComponent>(target, out var doorBoltComp) && doorBoltComp.BoltsDown)
  376. _door.SetBoltsDown((target, doorBoltComp), false, predicted: true);
  377. if (TryComp<DoorComponent>(target, out var doorComp) && doorComp.State is not DoorState.Open)
  378. _door.StartOpening(target);
  379. if (TryComp<LockComponent>(target, out var lockComp) && lockComp.Locked)
  380. _lock.Unlock(target, args.Performer, lockComp);
  381. }
  382. }
  383. // End Knock Spells
  384. #endregion
  385. #region Charge Spells
  386. // TODO: Future support to charge other items
  387. private void OnChargeSpell(ChargeSpellEvent ev)
  388. {
  389. if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || !TryComp<HandsComponent>(ev.Performer, out var handsComp))
  390. return;
  391. EntityUid? wand = null;
  392. foreach (var item in _hands.EnumerateHeld(ev.Performer, handsComp))
  393. {
  394. if (!_tag.HasTag(item, ev.WandTag))
  395. continue;
  396. wand = item;
  397. }
  398. ev.Handled = true;
  399. Speak(ev);
  400. if (wand == null || !TryComp<BasicEntityAmmoProviderComponent>(wand, out var basicAmmoComp) || basicAmmoComp.Count == null)
  401. return;
  402. _gunSystem.UpdateBasicEntityAmmoCount(wand.Value, basicAmmoComp.Count.Value + ev.Charge, basicAmmoComp);
  403. }
  404. // End Charge Spells
  405. #endregion
  406. #region Global Spells
  407. // TODO: Change this into a "StartRuleAction" when actions with multiple events are supported
  408. protected virtual void OnRandomGlobalSpawnSpell(RandomGlobalSpawnSpellEvent ev)
  409. {
  410. if (!_net.IsServer || ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || ev.Spawns is not { } spawns)
  411. return;
  412. ev.Handled = true;
  413. Speak(ev);
  414. var allHumans = _mind.GetAliveHumans();
  415. foreach (var human in allHumans)
  416. {
  417. if (!human.Comp.OwnedEntity.HasValue)
  418. continue;
  419. var ent = human.Comp.OwnedEntity.Value;
  420. if (_tag.HasTag(ent, "InvalidForGlobalSpawnSpell"))
  421. continue;
  422. var mapCoords = _transform.GetMapCoordinates(ent);
  423. foreach (var spawn in EntitySpawnCollection.GetSpawns(spawns, _random))
  424. {
  425. var spawned = Spawn(spawn, mapCoords);
  426. _hands.PickupOrDrop(ent, spawned);
  427. }
  428. }
  429. _audio.PlayGlobal(ev.Sound, ev.Performer);
  430. }
  431. #endregion
  432. #region Mindswap Spells
  433. private void OnMindSwapSpell(MindSwapSpellEvent ev)
  434. {
  435. if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
  436. return;
  437. ev.Handled = true;
  438. Speak(ev);
  439. // Need performer mind, but target mind is unnecessary, such as taking over a NPC
  440. // Need to get target mind before putting performer mind into their body if they have one
  441. // Thus, assign bool before first transfer, then check afterwards
  442. if (!_mind.TryGetMind(ev.Performer, out var perMind, out var perMindComp))
  443. return;
  444. var tarHasMind = _mind.TryGetMind(ev.Target, out var tarMind, out var tarMindComp);
  445. _mind.TransferTo(perMind, ev.Target);
  446. if (tarHasMind)
  447. {
  448. _mind.TransferTo(tarMind, ev.Performer);
  449. }
  450. _stun.TryParalyze(ev.Target, ev.TargetStunDuration, true);
  451. _stun.TryParalyze(ev.Performer, ev.PerformerStunDuration, true);
  452. }
  453. #endregion
  454. // End Spells
  455. #endregion
  456. // When any spell is cast it will raise this as an event, so then it can be played in server or something. At least until chat gets moved to shared
  457. // TODO: Temp until chat is in shared
  458. private void Speak(BaseActionEvent args)
  459. {
  460. if (args is not ISpeakSpell speak || string.IsNullOrWhiteSpace(speak.Speech))
  461. return;
  462. var ev = new SpeakSpellEvent(args.Performer, speak.Speech);
  463. RaiseLocalEvent(ref ev);
  464. }
  465. }