CatStation/Content.Shared/Guardian/GuardianSystem.cs
Whatstone 45f91a84a4
Bugfix: Guardian popup spam, mech attack blocker (#45013)
* guardian popup fixes

* actually use the entityqueries

* finish a thought challenge - difficulty impossible

* curse of fildrance

* can't believe he's done this
2026-07-29 20:48:46 +00:00

432 lines
15 KiB
C#

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;
/// <summary>
/// A guardian has a host it's attached to that it fights for. A fighting spirit.
/// </summary>
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<GuardianComponent> _guardianQuery;
[Dependency] private EntityQuery<GuardianHostComponent> _guardianHostQuery;
[SubscribeLocalEvent]
private void OnGuardianShutdown(Entity<GuardianComponent> 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<GuardianHostComponent> 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<GuardianComponent> 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<GuardianComponent> 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<GuardianHostComponent> ent, ref ComponentInit args)
{
ent.Comp.GuardianContainer = _container.EnsureContainer<ContainerSlot>(ent.Owner, "GuardianContainer");
_action.AddAction(ent.Owner, ref ent.Comp.ActionEntity, ent.Comp.Action);
}
[SubscribeLocalEvent]
private void OnHostShutdown(Entity<GuardianHostComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.HostedGuardian is not { } guardian)
return;
// Ensure held items are dropped before deleting guardian.
if (HasComp<HandsComponent>(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<GuardianComponent> 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<GuardianHostComponent> ent,
ref MechPilotRelayedEvent<GettingAttackedAttemptEvent> 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<GuardianHostComponent> 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));
}
/// <summary>
/// Adds the guardian host component to the user and spawns the guardian inside said component.
/// </summary>
[SubscribeLocalEvent]
private void OnCreatorUse(Entity<GuardianCreatorComponent> ent, ref UseInHandEvent args)
{
if (args.Handled)
return;
args.Handled = true;
UseCreator(args.User, args.User, ent);
}
[SubscribeLocalEvent]
private void OnCreatorInteract(Entity<GuardianCreatorComponent> 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<GuardianCreatorComponent> ent)
{
if (ent.Comp.Used)
{
_popup.PopupEntity(Loc.GetString(ent.Comp.EmptyPopup), user, user);
return;
}
// Can only inject things with the component...
if (!HasComp<CanHostGuardianComponent>(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<GuardianCreatorComponent> 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<GuardianHostComponent>(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;
}
/// <summary>
/// Triggers when the host receives damage which puts the host in either critical or killed state.
/// </summary>
[SubscribeLocalEvent]
private void OnHostStateChange(Entity<GuardianHostComponent> 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<GuardianHostComponent>(ent.Owner);
}
}
/// <summary>
/// Handles guardian receiving damage and splitting it with the host according to his defense percent.
/// </summary>
[SubscribeLocalEvent]
private void OnGuardianDamaged(Entity<GuardianComponent> 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);
}
/// <summary>
/// Triggers while trying to examine an activator to see if it's used.
/// </summary>
[SubscribeLocalEvent]
private void OnCreatorExamine(Entity<GuardianCreatorComponent> ent, ref ExaminedEvent args)
{
if (!ent.Comp.Used)
return;
args.PushMarkup(Loc.GetString(ent.Comp.EmptyExamine));
}
/// <summary>
/// Called every time the host moves, to make sure the host and the guardian are not too far away from each other.
/// </summary>
[SubscribeLocalEvent]
private void OnHostMove(Entity<GuardianHostComponent> ent, ref MoveEvent args)
{
if (!_guardianQuery.TryComp(ent.Comp.HostedGuardian, out GuardianComponent? guardianComponent) ||
!guardianComponent.GuardianLoose)
{
return;
}
CheckGuardianMove(ent.Owner, ent.Comp.HostedGuardian.Value);
}
/// <summary>
/// Called every time the guardian moves: makes sure it's not out of it's allowed distance.
/// </summary>
[SubscribeLocalEvent]
private void OnGuardianMove(Entity<GuardianComponent> ent, ref MoveEvent args)
{
if (!ent.Comp.GuardianLoose)
return;
if (ent.Comp.Host == null)
return;
CheckGuardianMove(ent.Comp.Host.Value, ent.Owner);
}
/// <summary>
/// Retract the guardian if either the host or the guardian move away from each other.
/// </summary>
private void CheckGuardianMove(
Entity<GuardianHostComponent?> host,
Entity<GuardianComponent?> 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<GuardianHostComponent> host, Entity<GuardianComponent> 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<GuardianHostComponent> host, Entity<GuardianComponent?> 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);
}
}