mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
changeling devour cleanup (#43451)
* devour cleanup * fix audio cancel * fix for transform as well
This commit is contained in:
parent
4fb78b777c
commit
85b4a5d0f6
4 changed files with 75 additions and 91 deletions
|
|
@ -1,12 +1,10 @@
|
|||
using Content.Shared.Changeling.Systems;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Changeling.Components;
|
||||
|
||||
|
|
@ -14,24 +12,24 @@ namespace Content.Shared.Changeling.Components;
|
|||
/// Component responsible for Changelings Devour attack. Including the amount of damage
|
||||
/// and how long it takes to devour someone
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
[Access(typeof(ChangelingDevourSystem))]
|
||||
public sealed partial class ChangelingDevourComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The Action for devouring
|
||||
/// The action for devouring.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId? ChangelingDevourAction = "ActionChangelingDevour";
|
||||
|
||||
/// <summary>
|
||||
/// The action entity associated with devouring
|
||||
/// The action entity associated with devouring.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityUid? ChangelingDevourActionEntity;
|
||||
|
||||
/// <summary>
|
||||
/// The whitelist of targets for devouring
|
||||
/// The whitelist of targets for devouring.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityWhitelist? Whitelist = new()
|
||||
|
|
@ -44,7 +42,7 @@ public sealed partial class ChangelingDevourComponent : Component
|
|||
};
|
||||
|
||||
/// <summary>
|
||||
/// The Sound to use during consumption of a victim
|
||||
/// The sound to use during consumption of a victim.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy.
|
||||
|
|
@ -54,7 +52,7 @@ public sealed partial class ChangelingDevourComponent : Component
|
|||
public SoundSpecifier? ConsumeNoise = new SoundCollectionSpecifier("ChangelingDevourConsume", AudioParams.Default.WithMaxDistance(6));
|
||||
|
||||
/// <summary>
|
||||
/// The Sound to use during the windup before consuming a victim
|
||||
/// The sound to use during the windup before consuming a victim.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy.
|
||||
|
|
@ -64,36 +62,31 @@ public sealed partial class ChangelingDevourComponent : Component
|
|||
public SoundSpecifier? DevourWindupNoise = new SoundCollectionSpecifier("ChangelingDevourWindup", AudioParams.Default.WithMaxDistance(6));
|
||||
|
||||
/// <summary>
|
||||
/// The time between damage ticks
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan DamageTimeBetweenTicks = TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <summary>
|
||||
/// The windup time before the changeling begins to engage in devouring the identity of a target
|
||||
/// The windup time before the changeling begins to engage in devouring the identity of a target.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan DevourWindupTime = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes to FULLY consume someones identity.
|
||||
/// The time it takes to consume someones identity.
|
||||
/// Starts after the windup.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan DevourConsumeTime = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// The Currently active devour sound in the world
|
||||
/// The currently active devour sound in the world.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? CurrentDevourSound;
|
||||
|
||||
/// <summary>
|
||||
/// The damage profile for a single tick of devour damage
|
||||
/// The damage dealt after the windup finished and devouring started.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public DamageSpecifier DamagePerTick = new()
|
||||
public DamageSpecifier WindupDamage = new()
|
||||
{
|
||||
DamageDict = new ()
|
||||
DamageDict = new()
|
||||
{
|
||||
{ "Slash", 10},
|
||||
{ "Piercing", 10 },
|
||||
|
|
@ -102,7 +95,21 @@ public sealed partial class ChangelingDevourComponent : Component
|
|||
};
|
||||
|
||||
/// <summary>
|
||||
/// The list of protective damage types capable of preventing a devour if over the threshold
|
||||
/// The damage dealt after the devouring is fully finished.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public DamageSpecifier DevourDamage = new()
|
||||
{
|
||||
DamageDict = new()
|
||||
{
|
||||
{ "Slash", 20},
|
||||
{ "Piercing", 20 },
|
||||
{ "Blunt", 10 },
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The list of protective damage types capable of preventing a devour if over the threshold.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public List<ProtoId<DamageTypePrototype>> ProtectiveDamageTypes = new()
|
||||
|
|
@ -113,13 +120,7 @@ public sealed partial class ChangelingDevourComponent : Component
|
|||
};
|
||||
|
||||
/// <summary>
|
||||
/// The next Tick to deal damage on (utilized during the consumption "do-during" (a do after with an attempt event))
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
|
||||
public TimeSpan NextTick = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The percentage of ANY brute damage resistance that will prevent devouring
|
||||
/// The percentage of ANY brute damage resistance that will prevent devouring.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float DevourPreventionPercentageThreshold = 0.1f;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ using Content.Shared.Actions;
|
|||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Armor;
|
||||
using Content.Shared.Atmos.Rotting;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
|
|
@ -19,13 +17,11 @@ using Content.Shared.Whitelist;
|
|||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Changeling.Systems;
|
||||
|
||||
public sealed class ChangelingDevourSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
|
|
@ -47,7 +43,6 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
|||
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourActionEvent>(OnDevourAction);
|
||||
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourWindupDoAfterEvent>(OnDevourWindup);
|
||||
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourConsumeDoAfterEvent>(OnDevourConsume);
|
||||
SubscribeLocalEvent<ChangelingDevourComponent, DoAfterAttemptEvent<ChangelingDevourConsumeDoAfterEvent>>(OnConsumeAttemptTick);
|
||||
SubscribeLocalEvent<ChangelingDevourComponent, ComponentShutdown>(OnShutdown);
|
||||
}
|
||||
|
||||
|
|
@ -64,29 +59,6 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
|||
}
|
||||
}
|
||||
|
||||
//TODO: Allow doafters to have proper update loop support. Attempt events should not be doing state changes.
|
||||
private void OnConsumeAttemptTick(Entity<ChangelingDevourComponent> ent,
|
||||
ref DoAfterAttemptEvent<ChangelingDevourConsumeDoAfterEvent> eventData)
|
||||
{
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
|
||||
if (curTime < ent.Comp.NextTick)
|
||||
return;
|
||||
|
||||
ConsumeDamageTick(eventData.Event.Target, ent.Comp, eventData.Event.User);
|
||||
ent.Comp.NextTick += ent.Comp.DamageTimeBetweenTicks;
|
||||
Dirty(ent, ent.Comp);
|
||||
}
|
||||
|
||||
private void ConsumeDamageTick(EntityUid? target, ChangelingDevourComponent comp, EntityUid? user)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
_damageable.ChangeDamage(target.Value, comp.DamagePerTick, true, true, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checkes if the targets outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured.
|
||||
/// </summary>
|
||||
|
|
@ -110,10 +82,13 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
|||
return false;
|
||||
}
|
||||
|
||||
// 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)
|
||||
|| !HasComp<ChangelingIdentityComponent>(ent))
|
||||
if (args.Handled
|
||||
|| _whitelistSystem.IsWhitelistFailOrNull(ent.Comp.Whitelist, args.Target)
|
||||
|| !HasComp<ChangelingIdentityComponent>(ent))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
|
@ -122,6 +97,12 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
|||
if (target == ent.Owner)
|
||||
return; // don't eat yourself
|
||||
|
||||
if (!_mobState.IsDead(target))
|
||||
{
|
||||
_popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-not-dead"), args.Performer, args.Performer, PopupType.Medium);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasComp<RottingComponent>(target))
|
||||
{
|
||||
_popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-rotting"), args.Performer, args.Performer, PopupType.Medium);
|
||||
|
|
@ -136,6 +117,7 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
|||
|
||||
if (_net.IsServer)
|
||||
{
|
||||
ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound);
|
||||
var pvsSound = _audio.PlayPvs(ent.Comp.DevourWindupNoise, ent);
|
||||
if (pvsSound != null)
|
||||
ent.Comp.CurrentDevourSound = pvsSound.Value.Entity;
|
||||
|
|
@ -146,7 +128,7 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
|||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, ent.Comp.DevourWindupTime, new ChangelingDevourWindupDoAfterEvent(), ent, target: target, used: ent)
|
||||
{
|
||||
BreakOnMove = true,
|
||||
BlockDuplicate = true,
|
||||
CancelDuplicate = true,
|
||||
DuplicateCondition = DuplicateConditions.None,
|
||||
});
|
||||
|
||||
|
|
@ -160,17 +142,21 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
|||
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)
|
||||
{
|
||||
var curTime = _timing.CurTime;
|
||||
args.Handled = true;
|
||||
|
||||
if (!Exists(ent.Comp.CurrentDevourSound))
|
||||
_audio.Stop(ent.Comp.CurrentDevourSound!);
|
||||
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.PopupPredicted(
|
||||
|
|
@ -188,44 +174,41 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
|||
ent.Comp.CurrentDevourSound = pvsSound.Value.Entity;
|
||||
}
|
||||
|
||||
|
||||
ent.Comp.NextTick = curTime + ent.Comp.DamageTimeBetweenTicks;
|
||||
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} began to devour {ToPrettyString(args.Target):player} identity");
|
||||
_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: args.Target,
|
||||
target: target,
|
||||
used: ent)
|
||||
{
|
||||
AttemptFrequency = AttemptFrequency.EveryTick,
|
||||
BreakOnMove = true,
|
||||
BlockDuplicate = true,
|
||||
CancelDuplicate = true,
|
||||
DuplicateCondition = DuplicateConditions.None,
|
||||
});
|
||||
}
|
||||
|
||||
// Second doafter finished.
|
||||
// Save the identity and deal more damage.
|
||||
private void OnDevourConsume(Entity<ChangelingDevourComponent> ent, ref ChangelingDevourConsumeDoAfterEvent args)
|
||||
{
|
||||
args.Handled = true;
|
||||
var target = args.Target;
|
||||
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
if (Exists(ent.Comp.CurrentDevourSound))
|
||||
_audio.Stop(ent.Comp.CurrentDevourSound!);
|
||||
ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound);
|
||||
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
if (!_mobState.IsDead((EntityUid)target))
|
||||
if (args.Target is not { } target)
|
||||
return;
|
||||
|
||||
_damageable.ChangeDamage(target, ent.Comp.DevourDamage, true, true, ent.Owner);
|
||||
|
||||
if (!_mobState.IsDead(target))
|
||||
{
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} unsuccessfully devoured {ToPrettyString(args.Target):player}'s identity");
|
||||
_popupSystem.PopupClient(Loc.GetString("changeling-devour-consume-failed-not-dead"), args.User, args.User, PopupType.Medium);
|
||||
_popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-not-dead"), args.User, args.User, PopupType.Medium);
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} unsuccessfully devoured {ToPrettyString(args.Target):player}'s identity");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -238,22 +221,22 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
|||
args.User,
|
||||
PopupType.LargeCaution);
|
||||
|
||||
if (_mobState.IsDead(target.Value)
|
||||
&& TryComp<BodyComponent>(target, out var body)
|
||||
if (_mobState.IsDead(target)
|
||||
&& HasComp<HumanoidProfileComponent>(target)
|
||||
&& TryComp<ChangelingIdentityComponent>(args.User, out var identityStorage))
|
||||
{
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(args.Target):player}'s identity");
|
||||
_changelingIdentitySystem.CloneToPausedMap((ent, identityStorage), target.Value);
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(target):player}'s identity");
|
||||
_changelingIdentitySystem.CloneToPausedMap((ent, identityStorage), target);
|
||||
|
||||
if (_inventorySystem.TryGetSlotEntity(target.Value, "jumpsuit", out var item)
|
||||
if (_inventorySystem.TryGetSlotEntity(target, "jumpsuit", out var item)
|
||||
&& TryComp<ButcherableComponent>(item, out var butcherable))
|
||||
RipClothing(target.Value, (item.Value, butcherable));
|
||||
RipClothing(target, (item.Value, butcherable));
|
||||
}
|
||||
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
// TODO: This should just be an API method in the butcher system
|
||||
private void RipClothing(EntityUid victim, Entity<ButcherableComponent> item)
|
||||
{
|
||||
var spawnEntities = EntitySpawnCollection.GetSpawns(item.Comp.SpawnedEntities, _robustRandom);
|
||||
|
|
|
|||
|
|
@ -99,7 +99,10 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
|
|||
PopupType.MediumCaution);
|
||||
|
||||
if (_net.IsServer)
|
||||
{
|
||||
ent.Comp.CurrentTransformSound = _audio.Stop(ent.Comp.CurrentTransformSound); // cancel any previous sounds first
|
||||
ent.Comp.CurrentTransformSound = _audio.PlayPvs(ent.Comp.TransformAttemptNoise, ent)?.Entity;
|
||||
}
|
||||
|
||||
if (TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player}) ");
|
||||
|
|
@ -146,9 +149,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
|
|||
ref ChangelingTransformDoAfterEvent args)
|
||||
{
|
||||
args.Handled = true;
|
||||
|
||||
if (Exists(ent.Comp.CurrentTransformSound))
|
||||
_audio.Stop(ent.Comp.CurrentTransformSound);
|
||||
ent.Comp.CurrentTransformSound = _audio.Stop(ent.Comp.CurrentTransformSound);
|
||||
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
roles-antag-changeling-name = Changeling
|
||||
roles-antag-changeling-objective = A intelligent predator that assumes the identities of its victims.
|
||||
|
||||
changeling-devour-attempt-failed-not-dead = This body yet lives! We cannot consume it alive!
|
||||
changeling-devour-attempt-failed-rotting = This corpse has only rotted biomass.
|
||||
changeling-devour-attempt-failed-protected = This victim's biomass is protected by armor!
|
||||
|
||||
|
|
@ -8,8 +9,6 @@ changeling-devour-begin-windup-self = Our uncanny mouth reveals itself with othe
|
|||
changeling-devour-begin-windup-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth reveals itself with otherworldly hunger.
|
||||
changeling-devour-begin-consume-self = The uncanny mouth digs deep into its victim.
|
||||
changeling-devour-begin-consume-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth digs deep into { POSS-ADJ($user) } victim.
|
||||
|
||||
changeling-devour-consume-failed-not-dead = This body yet lives! We cannot consume it alive!
|
||||
changeling-devour-consume-complete-self = Our uncanny mouth retreats, biomass consumed.
|
||||
changeling-devour-consume-complete-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth retreats.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue