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