using Content.Shared.Actions; using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.Examine; using Content.Shared.Gibbing; using Content.Shared.Guardian.Components; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Mech.EntitySystems; using Content.Shared.Mobs; using Content.Shared.Popups; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Shared.Guardian; /// /// A guardian has a host it's attached to that it fights for. A fighting spirit. /// public sealed partial class GuardianSystem : EntitySystem { [Dependency] private DamageableSystem _damage = default!; [Dependency] private GibbingSystem _gibbing = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private SharedActionsSystem _action = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; [Dependency] private SharedHandsSystem _hands = default!; [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private EntityQuery _guardianQuery; [Dependency] private EntityQuery _guardianHostQuery; [SubscribeLocalEvent] private void OnGuardianShutdown(Entity ent, ref ComponentShutdown args) { if (!_guardianHostQuery.TryComp(ent, out GuardianHostComponent? hostComponent)) return; _container.Remove(ent.Owner, hostComponent.GuardianContainer); hostComponent.HostedGuardian = null; PredictedDel(hostComponent.ActionEntity); hostComponent.ActionEntity = null; ent.Comp.Host = null; Dirty(ent); Dirty(ent, hostComponent); } [SubscribeLocalEvent] private void OnPerformAction(Entity ent, ref GuardianToggleActionEvent args) { if (args.Handled) return; if (_container.IsEntityInContainer(ent.Owner)) { _popup.PopupEntity(Loc.GetString("guardian-inside-container"), ent.Owner, ent.Owner); return; } if (ent.Comp.HostedGuardian != null) ToggleGuardian(ent); args.Handled = true; } [SubscribeLocalEvent] private void OnGuardianPlayerDetached(Entity ent, ref PlayerDetachedEvent args) { if (!_guardianHostQuery.TryComp(ent.Comp.Host, out GuardianHostComponent? hostComponent) || TerminatingOrDeleted(ent.Owner)) { PredictedQueueDel(ent.Owner); ent.Comp.Host = null; Dirty(ent); return; } RetractGuardian((ent.Comp.Host.Value, hostComponent), (ent.Owner, ent.Comp)); } [SubscribeLocalEvent] private void OnGuardianPlayerAttached(Entity ent, ref PlayerAttachedEvent args) { var host = ent.Comp.Host; if (!_guardianHostQuery.HasComp(host)) { PredictedQueueDel(ent.Owner); ent.Comp.Host = null; Dirty(ent); return; } _popup.PopupEntity(Loc.GetString("guardian-available"), host.Value, host.Value); } [SubscribeLocalEvent] private void OnHostInit(Entity ent, ref ComponentInit args) { ent.Comp.GuardianContainer = _container.EnsureContainer(ent.Owner, "GuardianContainer"); _action.AddAction(ent.Owner, ref ent.Comp.ActionEntity, ent.Comp.Action); } [SubscribeLocalEvent] private void OnHostShutdown(Entity ent, ref ComponentShutdown args) { if (ent.Comp.HostedGuardian is not { } guardian) return; // Ensure held items are dropped before deleting guardian. if (HasComp(guardian)) _gibbing.Gib(ent.Comp.HostedGuardian.Value); PredictedQueueDel(guardian); ent.Comp.HostedGuardian = null; PredictedDel(ent.Comp.ActionEntity); ent.Comp.ActionEntity = null; Dirty(ent); } [SubscribeLocalEvent] private void OnGuardianAttackAttempt(Entity ent, ref AttackAttemptEvent args) { if (args.Cancelled || args.Target != ent.Comp.Host) return; if (_timing.CurTime >= ent.Comp.LastAttackPopupTime + ent.Comp.AttackPopupDelay) { _popup.PopupCursor(Loc.GetString("guardian-attack-host"), ent.Owner, PopupType.LargeCaution); } ent.Comp.LastAttackPopupTime = _timing.CurTime; args.Cancel(); } [SubscribeLocalEvent] private void OnPilotAttackAttempt(Entity ent, ref MechPilotRelayedEvent args) { if (args.Args.Cancelled || args.Args.Attacker != ent.Comp.HostedGuardian) return; if (_guardianQuery.TryComp(args.Args.Attacker, out var guardian)) { if (_timing.CurTime >= guardian.LastAttackPopupTime + guardian.AttackPopupDelay) { _popup.PopupCursor(Loc.GetString("guardian-attack-host"), args.Args.Attacker, PopupType.LargeCaution); } guardian.LastAttackPopupTime = _timing.CurTime; } args.Args.Cancelled = true; } [SubscribeLocalEvent] private void ToggleGuardian(Entity ent) { if (!_guardianQuery.TryComp(ent.Comp.HostedGuardian, out var guardianComponent)) return; if (guardianComponent.GuardianLoose) RetractGuardian(ent, (ent.Comp.HostedGuardian.Value, guardianComponent)); else ReleaseGuardian(ent, (ent.Comp.HostedGuardian.Value, guardianComponent)); } /// /// Adds the guardian host component to the user and spawns the guardian inside said component. /// [SubscribeLocalEvent] private void OnCreatorUse(Entity ent, ref UseInHandEvent args) { if (args.Handled) return; args.Handled = true; UseCreator(args.User, args.User, ent); } [SubscribeLocalEvent] private void OnCreatorInteract(Entity ent, ref AfterInteractEvent args) { if (args.Handled || args.Target == null || !args.CanReach) return; args.Handled = true; UseCreator(args.User, args.Target.Value, ent); } private void UseCreator(EntityUid user, EntityUid target, Entity ent) { if (ent.Comp.Used) { _popup.PopupEntity(Loc.GetString(ent.Comp.EmptyPopup), user, user); return; } // Can only inject things with the component... if (!HasComp(target)) { var msg = Loc.GetString("guardian-activator-invalid-target", ("entity", Identity.Entity(target, EntityManager, user))); _popup.PopupEntity(msg, user, user); return; } // If user is already a host don't duplicate. if (_guardianHostQuery.HasComp(target)) { _popup.PopupEntity(Loc.GetString("guardian-already-present-invalid-creation"), user, user); return; } _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, ent.Comp.InjectionDelay, new GuardianCreatorDoAfterEvent(), ent, target: target, used: ent) { BreakOnMove = true, NeedHand = true, BreakOnHandChange = true }); } [SubscribeLocalEvent] private void OnDoAfter(Entity ent, ref GuardianCreatorDoAfterEvent args) { if (args.Handled || args.Args.Target == null || args.Cancelled || ent.Comp.Deleted || ent.Comp.Used) return; if (!_hands.IsHolding(args.Args.User, ent.Owner) || _guardianHostQuery.HasComp(args.Args.Target)) return; var hostXform = Transform(args.Args.Target.Value); var host = EnsureComp(args.Args.Target.Value); // Use map position so it's not inadvertently parented to the host + if it's in a container it spawns outside I guess. var guardian = PredictedSpawnAtPosition(ent.Comp.GuardianProto, _transform.GetMoverCoordinates(args.Args.Target.Value, xform: hostXform)); _container.Insert(guardian, host.GuardianContainer); host.HostedGuardian = guardian; if (_guardianQuery.TryComp(guardian, out GuardianComponent? guardianComp)) { guardianComp.Host = args.Args.Target.Value; Dirty(guardian, guardianComp); _audio.PlayPredicted(ent.Comp.UsedSound, ent.Owner, args.Args.Target); _popup.PopupEntity(Loc.GetString(ent.Comp.GuardianHauntedPopup), args.Args.Target.Value, args.Args.Target.Value); // Exhaust the activator ent.Comp.Used = true; } else { Log.Error($"Tried to spawn a guardian that doesn't have {nameof(GuardianComponent)}"); PredictedDel(guardian); } Dirty(ent); args.Handled = true; } /// /// Triggers when the host receives damage which puts the host in either critical or killed state. /// [SubscribeLocalEvent] private void OnHostStateChange(Entity ent, ref MobStateChangedEvent args) { if (ent.Comp.HostedGuardian == null || !_guardianQuery.TryComp(ent.Comp.HostedGuardian, out GuardianComponent? guardianComp)) return; if (args.NewMobState == MobState.Critical) { _popup.PopupEntity(Loc.GetString(ent.Comp.GuardianHostCritWarn), ent.Comp.HostedGuardian.Value, ent.Comp.HostedGuardian.Value); _audio.PlayPredicted(guardianComp.CriticalSound, ent.Comp.HostedGuardian.Value, args.Target); } else if (args.NewMobState == MobState.Dead) { _audio.PlayPredicted(guardianComp.DeathSound, ent.Owner, args.Target); RemComp(ent.Owner); } } /// /// Handles guardian receiving damage and splitting it with the host according to his defense percent. /// [SubscribeLocalEvent] private void OnGuardianDamaged(Entity ent, ref DamageDealtEvent args) { if (_timing.ApplyingState) return; if (args.Damage == null! || ent.Comp.Host == null || ent.Comp.DamageShare == 0) return; _damage.ChangeDamage( ent.Comp.Host.Value, args.Damage * ent.Comp.DamageShare, origin: args.Origin, ignoreResistances: true, interruptsDoAfters: false); _popup.PopupEntity(Loc.GetString(ent.Comp.GuardianDamagePopup), ent.Comp.Host.Value, ent.Comp.Host.Value); } /// /// Triggers while trying to examine an activator to see if it's used. /// [SubscribeLocalEvent] private void OnCreatorExamine(Entity ent, ref ExaminedEvent args) { if (!ent.Comp.Used) return; args.PushMarkup(Loc.GetString(ent.Comp.EmptyExamine)); } /// /// Called every time the host moves, to make sure the host and the guardian are not too far away from each other. /// [SubscribeLocalEvent] private void OnHostMove(Entity ent, ref MoveEvent args) { if (!_guardianQuery.TryComp(ent.Comp.HostedGuardian, out GuardianComponent? guardianComponent) || !guardianComponent.GuardianLoose) { return; } CheckGuardianMove(ent.Owner, ent.Comp.HostedGuardian.Value); } /// /// Called every time the guardian moves: makes sure it's not out of it's allowed distance. /// [SubscribeLocalEvent] private void OnGuardianMove(Entity ent, ref MoveEvent args) { if (!ent.Comp.GuardianLoose) return; if (ent.Comp.Host == null) return; CheckGuardianMove(ent.Comp.Host.Value, ent.Owner); } /// /// Retract the guardian if either the host or the guardian move away from each other. /// private void CheckGuardianMove( Entity host, Entity guardian, TransformComponent? hostXform = null, TransformComponent? guardianXform = null) { if (TerminatingOrDeleted(guardian.Owner) || TerminatingOrDeleted(host.Owner)) return; if (!Resolve(host.Owner, ref host.Comp, ref hostXform) || !Resolve(guardian.Owner, ref guardian.Comp, ref guardianXform)) { return; } if (!guardian.Comp.GuardianLoose) return; if (!_transform.InRange(guardianXform.Coordinates, hostXform.Coordinates, guardian.Comp.DistanceAllowed)) RetractGuardian((host.Owner, host.Comp), guardian); } private void ReleaseGuardian(Entity host, Entity guardian) { _audio.PlayPredicted(host.Comp.ReleaseSound, guardian, host); if (guardian.Comp.GuardianLoose) { DebugTools.Assert(!host.Comp.GuardianContainer.Contains(guardian)); return; } DebugTools.Assert(host.Comp.GuardianContainer.Contains(guardian)); _container.Remove(guardian.Owner, host.Comp.GuardianContainer); DebugTools.Assert(!host.Comp.GuardianContainer.Contains(guardian)); guardian.Comp.GuardianLoose = true; Dirty(host); Dirty(guardian); } private void RetractGuardian(Entity host, Entity guardian) { if (_timing.ApplyingState) return; if (!Resolve(guardian, ref guardian.Comp)) return; if (!guardian.Comp.GuardianLoose) { DebugTools.Assert(host.Comp.GuardianContainer.Contains(guardian)); return; } _container.Insert(guardian.Owner, host.Comp.GuardianContainer); _popup.PopupEntity(Loc.GetString(host.Comp.GuardianHostRecall), host.Owner, host.Owner); guardian.Comp.GuardianLoose = false; Dirty(host); Dirty(guardian); } }