using System.Numerics; using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Server.Chat.Systems; using Content.Server.Doors.Systems; using Content.Server.Magic.Components; using Content.Server.Magic.Events; using Content.Server.Weapons.Ranged.Systems; using Content.Shared.Actions; using Content.Shared.Actions.ActionTypes; using Content.Shared.Body.Components; using Content.Shared.Coordinates.Helpers; using Content.Shared.DoAfter; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; using Content.Shared.Interaction.Events; using Content.Shared.Magic; using Content.Shared.Maps; using Content.Shared.Physics; using Content.Shared.Spawners.Components; using Content.Shared.Storage; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Manager.Exceptions; namespace Content.Server.Magic; /// /// Handles learning and using spells (actions) /// public sealed class MagicSystem : EntitySystem { [Dependency] private readonly ISerializationManager _seriMan = default!; [Dependency] private readonly IComponentFactory _compFact = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly DoorBoltSystem _boltsSystem = default!; [Dependency] private readonly BodySystem _bodySystem = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedDoorSystem _doorSystem = default!; [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly GunSystem _gunSystem = default!; [Dependency] private readonly PhysicsSystem _physics = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly ChatSystem _chat = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnUse); SubscribeLocalEvent(OnDoAfter); SubscribeLocalEvent(OnInstantSpawn); SubscribeLocalEvent(OnTeleportSpell); SubscribeLocalEvent(OnKnockSpell); SubscribeLocalEvent(OnSmiteSpell); SubscribeLocalEvent(OnWorldSpawn); SubscribeLocalEvent(OnProjectileSpell); SubscribeLocalEvent(OnChangeComponentsSpell); } private void OnDoAfter(EntityUid uid, SpellbookComponent component, DoAfterEvent args) { if (args.Handled || args.Cancelled) return; _actionsSystem.AddActions(args.Args.User, component.Spells, component.LearnPermanently ? null : uid); args.Handled = true; } private void OnInit(EntityUid uid, SpellbookComponent component, ComponentInit args) { //Negative charges means the spell can be used without it running out. foreach (var (id, charges) in component.WorldSpells) { var spell = new WorldTargetAction(_prototypeManager.Index(id)); _actionsSystem.SetCharges(spell, charges < 0 ? null : charges); component.Spells.Add(spell); } foreach (var (id, charges) in component.InstantSpells) { var spell = new InstantAction(_prototypeManager.Index(id)); _actionsSystem.SetCharges(spell, charges < 0 ? null : charges); component.Spells.Add(spell); } foreach (var (id, charges) in component.EntitySpells) { var spell = new EntityTargetAction(_prototypeManager.Index(id)); _actionsSystem.SetCharges(spell, charges < 0 ? null : charges); component.Spells.Add(spell); } } private void OnUse(EntityUid uid, SpellbookComponent component, UseInHandEvent args) { if (args.Handled) return; AttemptLearn(uid, component, args); args.Handled = true; } private void AttemptLearn(EntityUid uid, SpellbookComponent component, UseInHandEvent args) { var doAfterEventArgs = new DoAfterArgs(args.User, component.LearnTime, new SpellbookDoAfterEvent(), uid, target: uid) { BreakOnTargetMove = true, BreakOnUserMove = true, BreakOnDamage = true, NeedHand = true //What, are you going to read with your eyes only?? }; _doAfter.TryStartDoAfter(doAfterEventArgs); } #region Spells /// /// Handles the instant action (i.e. on the caster) attempting to spawn an entity. /// private void OnInstantSpawn(InstantSpawnSpellEvent args) { if (args.Handled) return; var transform = Transform(args.Performer); foreach (var position in GetSpawnPositions(transform, args.Pos)) { var ent = Spawn(args.Prototype, position.SnapToGrid(EntityManager, _mapManager)); if (args.PreventCollideWithCaster) { var comp = EnsureComp(ent); comp.Uid = args.Performer; } } Speak(args); args.Handled = true; } private void OnProjectileSpell(ProjectileSpellEvent ev) { if (ev.Handled) return; ev.Handled = true; Speak(ev); var xform = Transform(ev.Performer); var userVelocity = _physics.GetMapLinearVelocity(ev.Performer); foreach (var pos in GetSpawnPositions(xform, ev.Pos)) { // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map. var mapPos = pos.ToMap(EntityManager); var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out _) ? pos.WithEntityId(gridUid, EntityManager) : new(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position); var ent = Spawn(ev.Prototype, spawnCoords); var direction = ev.Target.ToMapPos(EntityManager, _transformSystem) - spawnCoords.ToMapPos(EntityManager, _transformSystem); _gunSystem.ShootProjectile(ent, direction, userVelocity, ev.Performer); } } private void OnChangeComponentsSpell(ChangeComponentsSpellEvent ev) { if (ev.Handled) return; ev.Handled = true; Speak(ev); foreach (var toRemove in ev.ToRemove) { if (_compFact.TryGetRegistration(toRemove, out var registration)) RemComp(ev.Target, registration.Type); } foreach (var (name, data) in ev.ToAdd) { if (HasComp(ev.Target, data.Component.GetType())) continue; var component = (Component) _compFact.GetComponent(name); component.Owner = ev.Target; var temp = (object) component; _seriMan.CopyTo(data.Component, ref temp); EntityManager.AddComponent(ev.Target, (Component) temp!); } } private List GetSpawnPositions(TransformComponent casterXform, MagicSpawnData data) { switch (data) { case TargetCasterPos: return new List(1) {casterXform.Coordinates}; case TargetInFront: { // This is shit but you get the idea. var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized()); if (!_mapManager.TryGetGrid(casterXform.GridUid, out var mapGrid)) return new List(); if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager)) return new List(); var tileIndex = tileReference.Value.GridIndices; var coords = mapGrid.GridTileToLocal(tileIndex); EntityCoordinates coordsPlus; EntityCoordinates coordsMinus; var dir = casterXform.LocalRotation.GetCardinalDir(); switch (dir) { case Direction.North: case Direction.South: { coordsPlus = mapGrid.GridTileToLocal(tileIndex + (1, 0)); coordsMinus = mapGrid.GridTileToLocal(tileIndex + (-1, 0)); return new List(3) { coords, coordsPlus, coordsMinus, }; } case Direction.East: case Direction.West: { coordsPlus = mapGrid.GridTileToLocal(tileIndex + (0, 1)); coordsMinus = mapGrid.GridTileToLocal(tileIndex + (0, -1)); return new List(3) { coords, coordsPlus, coordsMinus, }; } } return new List(); } default: throw new ArgumentOutOfRangeException(); } } /// /// Teleports the user to the clicked location /// /// private void OnTeleportSpell(TeleportSpellEvent args) { if (args.Handled) return; var transform = Transform(args.Performer); if (transform.MapID != args.Target.GetMapId(EntityManager)) return; _transformSystem.SetCoordinates(args.Performer, args.Target); transform.AttachToGridOrMap(); _audio.PlayPvs(args.BlinkSound, args.Performer, AudioParams.Default.WithVolume(args.BlinkVolume)); Speak(args); args.Handled = true; } /// /// Opens all doors within range /// /// private void OnKnockSpell(KnockSpellEvent args) { if (args.Handled) return; args.Handled = true; Speak(args); //Get the position of the player var transform = Transform(args.Performer); var coords = transform.Coordinates; _audio.PlayPvs(args.KnockSound, args.Performer, AudioParams.Default.WithVolume(args.KnockVolume)); //Look for doors and don't open them if they're already open. foreach (var entity in _lookup.GetEntitiesInRange(coords, args.Range)) { if (TryComp(entity, out var bolts)) _boltsSystem.SetBoltsDown(entity, bolts, false); if (TryComp(entity, out var doorComp) && doorComp.State is not DoorState.Open) _doorSystem.StartOpening(doorComp.Owner); } } private void OnSmiteSpell(SmiteSpellEvent ev) { if (ev.Handled) return; ev.Handled = true; Speak(ev); var direction = Transform(ev.Target).MapPosition.Position - Transform(ev.Performer).MapPosition.Position; var impulseVector = direction * 10000; _physics.ApplyLinearImpulse(ev.Target, impulseVector); if (!TryComp(ev.Target, out var body)) return; var ents = _bodySystem.GibBody(ev.Target, true, body); if (!ev.DeleteNonBrainParts) return; foreach (var part in ents) { // just leaves a brain and clothes if (HasComp(part) && !HasComp(part)) { QueueDel(part); } } } /// /// Spawns entity prototypes from a list within range of click. /// /// /// It will offset mobs after the first mob based on the OffsetVector2 property supplied. /// /// The Spawn Spell Event args. private void OnWorldSpawn(WorldSpawnSpellEvent args) { if (args.Handled) return; var targetMapCoords = args.Target; SpawnSpellHelper(args.Contents, targetMapCoords, args.Lifetime, args.Offset); Speak(args); args.Handled = true; } /// /// Loops through a supplied list of entity prototypes and spawns them /// /// /// If an offset of 0, 0 is supplied then the entities will all spawn on the same tile. /// Any other offset will spawn entities starting from the source Map Coordinates and will increment the supplied /// offset /// /// The list of Entities to spawn in /// Map Coordinates where the entities will spawn /// Check to see if the entities should self delete /// A Vector2 offset that the entities will spawn in private void SpawnSpellHelper(List entityEntries, EntityCoordinates entityCoords, float? lifetime, Vector2 offsetVector2) { var getProtos = EntitySpawnCollection.GetSpawns(entityEntries, _random); var offsetCoords = entityCoords; foreach (var proto in getProtos) { // TODO: Share this code with instant because they're both doing similar things for positioning. var entity = Spawn(proto, offsetCoords); offsetCoords = offsetCoords.Offset(offsetVector2); if (lifetime != null) { var comp = EnsureComp(entity); comp.Lifetime = lifetime.Value; } } } #endregion private void Speak(BaseActionEvent args) { if (args is not ISpeakSpell speak || string.IsNullOrWhiteSpace(speak.Speech)) return; _chat.TrySendInGameICMessage(args.Performer, Loc.GetString(speak.Speech), InGameICChatType.Speak, false); } }