mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
324 lines
14 KiB
C#
324 lines
14 KiB
C#
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<ChangelingDevourComponent, MapInitEvent>(OnMapInit);
|
|
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourActionEvent>(OnDevourAction);
|
|
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourWindupDoAfterEvent>(OnDevourWindup);
|
|
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourConsumeDoAfterEvent>(OnDevourConsume);
|
|
SubscribeLocalEvent<ChangelingDevourComponent, ComponentShutdown>(OnShutdown);
|
|
}
|
|
|
|
private void OnMapInit(Entity<ChangelingDevourComponent> ent, ref MapInitEvent args)
|
|
{
|
|
_actionsSystem.AddAction(ent, ref ent.Comp.ChangelingDevourActionEntity, ent.Comp.ChangelingDevourAction);
|
|
}
|
|
|
|
private void OnShutdown(Entity<ChangelingDevourComponent> 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<ChangelingDevourComponent> 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<ChangelingDevourComponent> 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<ChangelingDevourComponent> 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<RecentlyDevouredComponent>(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<BloodstreamComponent>(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<StoreComponent>(ent, out var store))
|
|
_store.TryAddCurrency(ent.Comp.DevourDnaReward, ent.Owner, store);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can the given changeling devour the given victim?
|
|
/// </summary>
|
|
public bool CanDevour(Entity<ChangelingDevourComponent?> 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<HumanoidProfileComponent>(victim))
|
|
{
|
|
if (showPopup)
|
|
_popupSystem.PopupEntity(Loc.GetString("changeling-devour-attempt-failed-cannot-devour"), changeling.Owner, changeling.Owner, PopupType.Medium);
|
|
return false;
|
|
}
|
|
|
|
if (HasComp<RecentlyDevouredComponent>(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<RottingComponent>(victim) || HasComp<ZombieComponent>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the target's outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured.
|
|
/// </summary>
|
|
/// <param name="target">The Targeted entity</param>
|
|
/// <param name="ent">Changelings Devour Component</param>
|
|
/// <returns>Is the target Protected from the attack</returns>
|
|
public bool IsTargetProtected(EntityUid target, Entity<ChangelingDevourComponent?> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether devouring this target has never been devoured by the changeling before.
|
|
/// </summary>
|
|
/// <param name="ent">The changeling.</param>
|
|
/// <param name="devoured">The target entity.</param>
|
|
/// <returns>True if the target was never devoured before, otherwise False.</returns>
|
|
public bool IsUniqueDevour(Entity<ChangelingIdentityComponent?> ent, EntityUid devoured)
|
|
{
|
|
if (!Resolve(ent, ref ent.Comp, false))
|
|
return false;
|
|
|
|
return !_changelingIdentity.TryGetDataFromOriginal(ent, devoured, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether devouring this entity will grant DNA to the changeling.
|
|
/// </summary>
|
|
/// <param name="ent">The changeling.</param>
|
|
/// <param name="devoured">The target entity.</param>
|
|
/// <returns>True if this target entity has granted the changeling DNA before, False otherwise.</returns>
|
|
public bool WillDevourGrantDna(Entity<ChangelingIdentityComponent?> 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;
|
|
}
|
|
}
|