using Content.Shared.Actions; using Content.Shared.Administration.Logs; using Content.Shared.Armor; using Content.Shared.Atmos.Rotting; using Content.Shared.Body.Components; using Content.Shared.Body.Systems; using Content.Shared.Changeling.Components; using Content.Shared.Store; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.Fluids; using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; using Content.Shared.Inventory; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; using Content.Shared.Store.Components; using Content.Shared.Whitelist; using Content.Shared.Zombies; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; namespace Content.Shared.Changeling.Systems; public sealed partial class ChangelingDevourSystem : EntitySystem { [Dependency] private BloodstreamSystem _bloodstream = default!; [Dependency] private DamageableSystem _damageable = default!; [Dependency] private EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private INetManager _net = default!; [Dependency] private ISharedAdminLogManager _adminLogger = default!; [Dependency] private MobStateSystem _mobState = default!; [Dependency] private SharedActionsSystem _actionsSystem = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedChangelingIdentitySystem _changelingIdentity = default!; [Dependency] private SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private SharedPopupSystem _popupSystem = default!; [Dependency] private SharedStoreSystem _store = default!; [Dependency] private SharedPuddleSystem _puddle = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnDevourAction); SubscribeLocalEvent(OnDevourWindup); SubscribeLocalEvent(OnDevourConsume); SubscribeLocalEvent(OnShutdown); } private void OnMapInit(Entity ent, ref MapInitEvent args) { _actionsSystem.AddAction(ent, ref ent.Comp.ChangelingDevourActionEntity, ent.Comp.ChangelingDevourAction); } private void OnShutdown(Entity ent, ref ComponentShutdown args) { if (ent.Comp.ChangelingDevourActionEntity != null) { _actionsSystem.RemoveAction(ent.Owner, ent.Comp.ChangelingDevourActionEntity); } } // The action was used. // Start the first doafter for the windup. private void OnDevourAction(Entity ent, ref ChangelingDevourActionEvent args) { if (args.Handled || _whitelistSystem.IsWhitelistFailOrNull(ent.Comp.Whitelist, args.Target)) return; args.Handled = true; var target = args.Target; if (!CanDevour(ent.AsNullable(), target)) return; if (_net.IsServer) { ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound); ent.Comp.CurrentDevourSound = _audio.PlayPvs(ent.Comp.DevourWindupNoise, ent)?.Entity; } _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ent:player} started changeling devour windup against {target:player}"); _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, ent.Comp.DevourWindupTime, new ChangelingDevourWindupDoAfterEvent(), ent, target: target, used: ent) { BreakOnMove = true, CancelDuplicate = true, DuplicateCondition = DuplicateConditions.None, ExamineText = ent.Comp.WindupExamine == null ? null : Loc.GetString(ent.Comp.WindupExamine, ("user", Identity.Entity(ent, EntityManager)), ("target", Identity.Entity(target, EntityManager))) }); var selfMessage = Loc.GetString("changeling-devour-begin-windup-self", ("user", Identity.Entity(ent.Owner, EntityManager))); var othersMessage = Loc.GetString("changeling-devour-begin-windup-others", ("user", Identity.Entity(ent.Owner, EntityManager))); _popupSystem.PopupEntity( selfMessage, othersMessage, args.Performer, args.Performer, PopupType.MediumCaution); } // First doafter finished. // Start the second doafter for the actual consumption and deal a small amount of damage. private void OnDevourWindup(Entity ent, ref ChangelingDevourWindupDoAfterEvent args) { args.Handled = true; ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound); if (args.Cancelled) return; if (args.Target is not { } target) return; _damageable.ChangeDamage(target, ent.Comp.WindupDamage, true, true, ent.Owner); var selfMessage = Loc.GetString("changeling-devour-begin-consume-self", ("user", Identity.Entity(ent.Owner, EntityManager))); var othersMessage = Loc.GetString("changeling-devour-begin-consume-others", ("user", Identity.Entity(ent.Owner, EntityManager))); _popupSystem.PopupEntity( selfMessage, othersMessage, ent.Owner, ent.Owner, PopupType.LargeCaution); if (_net.IsServer) ent.Comp.CurrentDevourSound = _audio.PlayPvs(ent.Comp.ConsumeNoise, ent)?.Entity; _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} began to devour {ToPrettyString(target):player}'s identity"); _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, ent.Comp.DevourConsumeTime, new ChangelingDevourConsumeDoAfterEvent(), ent, target: target, used: ent) { BreakOnMove = true, CancelDuplicate = true, DuplicateCondition = DuplicateConditions.None, ExamineText = ent.Comp.DevourExamine == null ? null : Loc.GetString(ent.Comp.DevourExamine, ("user", Identity.Entity(ent, EntityManager)), ("target", Identity.Entity(target, EntityManager))) }); } // Second doafter finished. // Save the identity and deal more damage. private void OnDevourConsume(Entity ent, ref ChangelingDevourConsumeDoAfterEvent args) { args.Handled = true; ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound); if (args.Cancelled) return; if (args.Target is not { } target) return; // Damage first before the CanDevour check to make sure they don't gib in-between and to kill them again in case they somehow revived. _damageable.ChangeDamage(target, ent.Comp.DevourDamage, true, true, ent.Owner); if (!CanDevour(ent.AsNullable(), target)) // Check again if the conditions are still met. return; var selfMessage = Loc.GetString("changeling-devour-consume-complete-self", ("user", Identity.Entity(ent.Owner, EntityManager))); var othersMessage = Loc.GetString("changeling-devour-consume-complete-others", ("user", Identity.Entity(ent.Owner, EntityManager))); _popupSystem.PopupEntity( selfMessage, othersMessage, ent.Owner, ent.Owner, PopupType.LargeCaution); _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(target):player}'s identity"); // A unique identity is separate from whether we have actually devoured this target before. var uniqueIdentity = IsUniqueDevour(ent.Owner, target); var willGrantDna = WillDevourGrantDna(ent.Owner, target); // Even if not unique, target is supposed to give us an identity if it is not currently in our identity list. var becomesIdentity = !_changelingIdentity.HasIdentity(ent.Owner, target); var ev = new ChangelingDevouredEvent(ent.Owner, target, becomesIdentity, uniqueIdentity, willGrantDna); RaiseLocalEvent(ent, ref ev, true); // We broadcast the event to allow relevant objectives to update. var devouredEv = new ChangelingGotDevouredEvent(ent.Owner, target, becomesIdentity, uniqueIdentity, willGrantDna); RaiseLocalEvent(target, ref devouredEv); // Don't broadcast this one, all neccessary data is in the previous event already. Just use that one if a broadcast is needed. EnsureComp(target); if (ent.Comp.DevourSpill != null) { // Spilled solution should have the same DNA as the changeling at the time of devouring var spill = ent.Comp.DevourSpill.Clone(); if (TryComp(ent, out var bloodstream)) spill.SetReagentData(_bloodstream.GetEntityBloodData((ent, bloodstream))); _puddle.TrySpillAt(target, spill, out _, false); } // Grants the DNA reward associated with a successful unique devour. if (willGrantDna && TryComp(ent, out var store)) _store.TryAddCurrency(ent.Comp.DevourDnaReward, ent.Owner, store); } /// /// Can the given changeling devour the given victim? /// public bool CanDevour(Entity changeling, EntityUid victim, bool checkDead = true, bool checkProtected = true, bool showPopup = true) { if (!Resolve(changeling, ref changeling.Comp)) return false; if (changeling.Owner == victim) return false; // Can't devour yourself. if (!HasComp(victim)) { if (showPopup) _popupSystem.PopupEntity(Loc.GetString("changeling-devour-attempt-failed-cannot-devour"), changeling.Owner, changeling.Owner, PopupType.Medium); return false; } if (HasComp(victim)) { if (showPopup) _popupSystem.PopupEntity(Loc.GetString("changeling-devour-attempt-failed-devoured-recently"), changeling.Owner, changeling.Owner, PopupType.Medium); return false; } if (checkDead && !_mobState.IsDead(victim)) { if (showPopup) _popupSystem.PopupEntity(Loc.GetString("changeling-devour-attempt-failed-not-dead"), changeling.Owner, changeling.Owner, PopupType.Medium); return false; } if (HasComp(victim) || HasComp(victim)) { if (showPopup) _popupSystem.PopupEntity(Loc.GetString("changeling-devour-attempt-failed-rotting"), changeling.Owner, changeling.Owner, PopupType.Medium); return false; } if (checkProtected && IsTargetProtected(victim, changeling)) { if (showPopup) _popupSystem.PopupEntity(Loc.GetString("changeling-devour-attempt-failed-protected"), changeling.Owner, changeling.Owner, PopupType.Medium); return false; } if (!_changelingIdentity.HasIdentity(changeling.Owner, victim) && !_changelingIdentity.HasFreeDisguiseSlot(changeling.Owner)) { if (showPopup) _popupSystem.PopupEntity(Loc.GetString("changeling-devour-attempt-failed-no-space"), changeling.Owner, changeling.Owner, PopupType.Medium); return false; } return true; } /// /// Checks if the target's outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured. /// /// The Targeted entity /// Changelings Devour Component /// Is the target Protected from the attack public bool IsTargetProtected(EntityUid target, Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return false; var ev = new CoefficientQueryEvent(SlotFlags.OUTERCLOTHING); RaiseLocalEvent(target, ev); foreach (var compProtectiveDamageType in ent.Comp.ProtectiveDamageTypes) { if (!ev.DamageModifiers.Coefficients.TryGetValue(compProtectiveDamageType, out var coefficient)) continue; if (coefficient < 1f - ent.Comp.DevourPreventionPercentageThreshold) return true; } return false; } /// /// Checks whether devouring this target has never been devoured by the changeling before. /// /// The changeling. /// The target entity. /// True if the target was never devoured before, otherwise False. public bool IsUniqueDevour(Entity ent, EntityUid devoured) { if (!Resolve(ent, ref ent.Comp, false)) return false; return !_changelingIdentity.TryGetDataFromOriginal(ent, devoured, out _); } /// /// Checks whether devouring this entity will grant DNA to the changeling. /// /// The changeling. /// The target entity. /// True if this target entity has granted the changeling DNA before, False otherwise. public bool WillDevourGrantDna(Entity ent, EntityUid devoured) { if (!Resolve(ent, ref ent.Comp, false)) return false; // This target was never devoured, so obviously it can grant us DNA. if (!_changelingIdentity.TryGetDataFromOriginal(ent, devoured, out var data)) return true; // If the entity was Devoured, it means it already granted DNA, so we return False. return !data.GrantedDna; } }