using System.Linq; using System.Numerics; using Content.Server._White.Wizard.Magic; using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Server.Chat.Systems; using Content.Server.Weapons.Ranged.Systems; using Content.Shared.Actions; using Content.Shared.Body.Components; using Content.Shared.Coordinates.Helpers; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; using Content.Shared.Lock; using Content.Shared.Magic; using Content.Shared.Magic.Events; using Content.Shared.Maps; using Content.Shared.Physics; using Content.Shared.Storage; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager; using Robust.Shared.Spawners; namespace Content.Server.Magic; /// /// Handles learning and using spells (actions) /// public sealed class MagicSystem : EntitySystem { [Dependency] private readonly ISerializationManager _serializationManager = default!; [Dependency] private readonly IComponentFactory _compFact = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly BodySystem _bodySystem = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedDoorSystem _doorSystem = 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!; [Dependency] private readonly WizardSpellsSystem _wizardSpells = default!; [Dependency] private readonly LockSystem _lock = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnInstantSpawn); SubscribeLocalEvent(OnTeleportSpell); SubscribeLocalEvent(OnKnockSpell); SubscribeLocalEvent(OnSmiteSpell); SubscribeLocalEvent(OnWorldSpawn); SubscribeLocalEvent(OnProjectileSpell); SubscribeLocalEvent(OnChangeComponentsSpell); } #region Spells 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) continue; 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); foreach (var pos in GetSpawnPositions(xform, ev.Pos)) { var mapPos = _transformSystem.ToMapCoordinates(pos); var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out var grid) ? pos.WithEntityId(gridUid, EntityManager) : new EntityCoordinates(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position); var userVelocity = Vector2.Zero; if (grid != null && TryComp(gridUid, out PhysicsComponent? physics)) userVelocity = physics.LinearVelocity; var ent = Spawn(ev.Prototype, spawnCoords); var direction = ev.Target.ToMapPos(EntityManager, _transformSystem) - spawnCoords.ToMapPos(EntityManager, _transformSystem); _gunSystem.ShootProjectile(ent, direction, userVelocity, ev.Performer, 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); var temp = (object) component; _serializationManager.CopyTo(data.Component, ref temp); EntityManager.AddComponent(ev.Target, (Component) temp!); } } 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); _transformSystem.AttachToGridOrMap(args.Performer); _audio.PlayPvs(args.BlinkSound, args.Performer, AudioParams.Default.WithVolume(args.BlinkVolume)); Speak(args); args.Handled = true; } private void OnKnockSpell(KnockSpellEvent args) { if (!_wizardSpells.CanCast(args)) // WD EDIT return; _wizardSpells.Cast(args); // WD EDIT var transform = Transform(args.Performer); var coords = transform.Coordinates; _audio.PlayPvs(args.KnockSound, args.Performer, AudioParams.Default.WithVolume(args.KnockVolume)); foreach (var entity in _lookup.GetEntitiesInRange(coords, args.Range)) { if (TryComp(entity, out var bolts)) _doorSystem.SetBoltsDown((entity, bolts), false); if (TryComp(entity, out var doorComp) && doorComp.State is not DoorState.Open) _doorSystem.StartOpening(entity); if (TryComp(entity, out var lockComp) && lockComp.Locked) _lock.Unlock(entity, null, lockComp); } } private void OnSmiteSpell(SmiteSpellEvent ev) { if (!_wizardSpells.CanCast(ev)) // WD EDIT return; _wizardSpells.Cast(ev); // WD EDIT var direction = _transformSystem.GetMapCoordinates(ev.Target).Position - _transformSystem.GetMapCoordinates(ev.Performer).Position; var impulseVector = direction * 10000; _physics.ApplyLinearImpulse(ev.Target, impulseVector); if (!TryComp(ev.Target, out var body)) return; var entities = _bodySystem.GibBody(ev.Target, true, body); if (!ev.DeleteNonBrainParts) return; foreach (var part in entities.Where(part => HasComp(part) && !HasComp(part))) { QueueDel(part); } } 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; } #endregion #region Helpers public List GetSpawnPositions(TransformComponent casterXform, MagicSpawnData data) { return data switch { TargetCasterPos => GetCasterPosition(casterXform), TargetInFront => GetPositionsInFront(casterXform), _ => throw new ArgumentOutOfRangeException() }; } public List GetCasterPosition(TransformComponent casterXform) { return [casterXform.Coordinates]; } public List GetPositionsInFront(TransformComponent casterXform) { var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized()); if (!TryComp(casterXform.GridUid, out var mapGrid) || !directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager)) { return new List(); } var tileIndex = tileReference.Value.GridIndices; var coords = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex); var directions = GetCardinalDirections(casterXform.LocalRotation.GetCardinalDir()); var spawnPositions = new List(3); foreach (var direction in directions) { var offset = GetOffsetForDirection(direction); var coordinates = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + offset); spawnPositions.Add(coordinates); } spawnPositions.Add(coords); return spawnPositions; } public IEnumerable GetCardinalDirections(Direction dir) { switch (dir) { case Direction.North: case Direction.South: return new[] { Direction.North, Direction.South }; case Direction.East: case Direction.West: return new[] { Direction.East, Direction.West }; default: return Array.Empty(); } } public (int, int) GetOffsetForDirection(Direction direction) { return direction switch { Direction.North => (1, 0), Direction.South => (-1, 0), Direction.East => (0, 1), Direction.West => (0, -1), _ => (0, 0) }; } public void SpawnSpellHelper(List entityEntries, EntityCoordinates entityCoords, float? lifetime, Vector2 offsetVector2) { var getPrototypes = EntitySpawnCollection.GetSpawns(entityEntries, _random); var offsetCoords = entityCoords; foreach (var proto in getPrototypes) { // 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; } } } 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); } #endregion }