diff --git a/Content.Client/Changeling/Systems/ChangelingIdentitySystem.cs b/Content.Client/Changeling/Systems/ChangelingIdentitySystem.cs
index 917eeefc08..1dbf2ad920 100644
--- a/Content.Client/Changeling/Systems/ChangelingIdentitySystem.cs
+++ b/Content.Client/Changeling/Systems/ChangelingIdentitySystem.cs
@@ -33,6 +33,7 @@ public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem
OriginalJob = identity.OriginalJob,
OriginalName = identity.OriginalName,
Starting = identity.Starting,
+ GrantedDna = identity.GrantedDna,
};
ent.Comp.ConsumedIdentities.Add(data);
diff --git a/Content.Server/Changeling/Systems/ChangelingIdentitySystem.cs b/Content.Server/Changeling/Systems/ChangelingIdentitySystem.cs
index 6f7556196c..2d2eb04348 100644
--- a/Content.Server/Changeling/Systems/ChangelingIdentitySystem.cs
+++ b/Content.Server/Changeling/Systems/ChangelingIdentitySystem.cs
@@ -24,8 +24,9 @@ public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem
Identity = GetNetEntity(identity.Identity),
Original = GetNetEntity(identity.Original),
OriginalJob = identity.OriginalJob,
- OriginalName = identity.Original != null ? Name(identity.Original.Value) : string.Empty,
+ OriginalName = identity.OriginalName,
Starting = identity.Starting,
+ GrantedDna = identity.GrantedDna,
};
sentIdentities.Add(netData);
diff --git a/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs b/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs
index 1f8b3c81d3..425d64f4a9 100644
--- a/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs
+++ b/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs
@@ -1,6 +1,8 @@
using Content.Shared.Changeling.Systems;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Content.Shared.Store;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
@@ -125,6 +127,15 @@ public sealed partial class ChangelingDevourComponent : Component
[DataField, AutoNetworkedField]
public float DevourPreventionPercentageThreshold = 0.1f;
+ ///
+ /// DNA awarded for successfully devouring a new identity.
+ ///
+ [DataField, AutoNetworkedField]
+ public Dictionary DevourDnaReward = new()
+ {
+ { "ChangelingDNA", 10 }
+ };
+
public override bool SendOnlyToOwner => true;
}
@@ -134,9 +145,10 @@ public sealed partial class ChangelingDevourComponent : Component
/// The changeling devouring this entity.
/// The entity that was devoured.
/// Whether the changeling is going to be given the target's identity after devouring.
-/// Whether this entity was eaten by the changeling before.
+/// Whether the changeling has never had the identity of this target before.
+/// Whether this devour has granted the changeling Dna.
[ByRefEvent]
-public record struct ChangelingDevouredEvent(EntityUid Changeling, EntityUid Devoured, bool ObtainedIdentity, bool Unique);
+public record struct ChangelingDevouredEvent(EntityUid Changeling, EntityUid Devoured, bool ObtainedIdentity, bool Unique, bool GrantedDna);
///
/// Event raised on an entity when devoured by a changeling.
@@ -144,6 +156,7 @@ public record struct ChangelingDevouredEvent(EntityUid Changeling, EntityUid Dev
/// The changeling devouring this entity.
/// The entity that was devoured.
/// Whether the changeling is going to be given the target's identity after devouring.
-/// Whether this entity was eaten by the changeling before.
+/// Whether the changeling has never had the identity of this target before.
+/// Whether this devour has granted the changeling Dna.
[ByRefEvent]
-public record struct ChangelingGotDevouredEvent(EntityUid Changeling, EntityUid Devoured, bool ObtainedIdentity, bool Unique);
+public record struct ChangelingGotDevouredEvent(EntityUid Changeling, EntityUid Devoured, bool ObtainedIdentity, bool Unique, bool GrantedDna);
diff --git a/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs b/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs
index f25489fe1f..7e7fa94b14 100644
--- a/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs
+++ b/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs
@@ -4,18 +4,12 @@ namespace Content.Shared.Changeling.Components;
///
/// Component used for marking entities devoured by a changeling.
-/// Used to prevent granting the identity several times.
+/// Used to track which changelings have an identity of this entity.
+/// Used for cleanup purposes.
///
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ChangelingDevouredComponent : Component
{
- ///
- /// Whether this entity has been devoured recently.
- /// Gets set back to False when the entity with this component becomes again.
- ///
- [DataField, AutoNetworkedField]
- public bool Recent;
-
///
/// HashSet of all changelings that have devoured this entity.
///
diff --git a/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs b/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs
index affec705ab..dcb65d5641 100644
--- a/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs
+++ b/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs
@@ -112,6 +112,12 @@ public sealed partial class ChangelingIdentityData
[DataField]
public bool Starting = false;
+ ///
+ /// Whether this identity has granted DNA after devour.
+ ///
+ [DataField]
+ public bool GrantedDna = false;
+
///
/// Convert to a string representation. This if for logging & debugging. This is not localized and should not be
/// shown to players.
@@ -143,4 +149,7 @@ public sealed partial class ChangelingNetworkedIdentityData
[DataField]
public bool Starting;
+
+ [DataField]
+ public bool GrantedDna;
}
diff --git a/Content.Shared/Changeling/Components/RecentlyDevouredComponent.cs b/Content.Shared/Changeling/Components/RecentlyDevouredComponent.cs
new file mode 100644
index 0000000000..a48565ee54
--- /dev/null
+++ b/Content.Shared/Changeling/Components/RecentlyDevouredComponent.cs
@@ -0,0 +1,9 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Changeling.Components;
+
+///
+/// Marker component for entities that were devoured recently and cannot be devoured again until revived.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class RecentlyDevouredComponent : Component;
diff --git a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs
index 6d07fb9378..63aad8b4d6 100644
--- a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs
+++ b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs
@@ -4,6 +4,7 @@ using Content.Shared.Administration.Logs;
using Content.Shared.Armor;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Changeling.Components;
+using Content.Shared.Store;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
@@ -12,6 +13,7 @@ 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 Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
@@ -20,16 +22,17 @@ namespace Content.Shared.Changeling.Systems;
public sealed class ChangelingDevourSystem : EntitySystem
{
+ [Dependency] private readonly DamageableSystem _damageable = default!;
+ [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentitySystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
- [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
- [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
- [Dependency] private readonly DamageableSystem _damageable = default!;
- [Dependency] private readonly MobStateSystem _mobState = default!;
- [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentitySystem = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly SharedStoreSystem _store = default!;
public override void Initialize()
{
@@ -167,17 +170,24 @@ public sealed class ChangelingDevourSystem : EntitySystem
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(target):player}'s identity");
- // If this entity has never been devoured before, it counts as unique.
- var unique = !_changelingIdentitySystem.TryGetDataFromOriginal(ent.Owner, target, out _);
+ // 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 = !HasIdentity(ent.Owner, target);
- var ev = new ChangelingDevouredEvent(ent.Owner, target, becomesIdentity, unique);
+ 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, unique);
+ 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);
+
+ // 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);
}
///
@@ -191,17 +201,6 @@ public sealed class ChangelingDevourSystem : EntitySystem
return changeling.Comp.ConsumedIdentities.FirstOrDefault(data => data.Original == devoured && data.Identity != null) != null;
}
- ///
- /// Has this entity been devoured by a changeling already before getting revived?
- ///
- public bool WasDevouredRecently(Entity entity)
- {
- if (!Resolve(entity, ref entity.Comp, false))
- return false;
-
- return entity.Comp.Recent;
- }
-
///
/// Can the given changeling devour the given victim?
///
@@ -220,7 +219,7 @@ public sealed class ChangelingDevourSystem : EntitySystem
return false;
}
- if (WasDevouredRecently(victim))
+ if (HasComp(victim))
{
if (showPopup)
_popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-devoured-recently"), changeling.Owner, changeling.Owner, PopupType.Medium);
@@ -282,16 +281,35 @@ public sealed class ChangelingDevourSystem : EntitySystem
}
///
- /// Checks whether this changeling has devoured the target entity at any point before.
+ /// Checks whether devouring this target has never been devoured by the changeling before.
///
/// The changeling.
/// The target entity.
- /// True if target was previously devoured, False otherwise.
+ /// 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 _changelingIdentitySystem.TryGetDataFromOriginal(ent, devoured, out _);
+ return !_changelingIdentitySystem.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 (!_changelingIdentitySystem.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;
}
}
diff --git a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
index 307c6a2132..a6c1379a7a 100644
--- a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
+++ b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
@@ -37,23 +37,21 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
SubscribeLocalEvent(OnStoredRemove);
SubscribeLocalEvent(OnDevouredShutdown);
- SubscribeLocalEvent(OnDevouredMobState);
+ SubscribeLocalEvent(OnRecentlyDevouredMobState);
}
private void OnDevouredEntity(Entity ent, ref ChangelingDevouredEvent args)
{
- // We're not supposed to be given an identity.
- if (!args.ObtainedIdentity)
- return;
+ if (args.ObtainedIdentity)
+ {
+ CloneToPausedMap(ent, args.Devoured);
+ AddDevouredReference(ent, args.Devoured);
+ }
- CloneToPausedMap(ent, args.Devoured);
-
- // We add a reference to ourselves to prevent repeated identity gain.
- var targetDevoured = EnsureComp(args.Devoured);
- targetDevoured.DevouredBy.Add(ent.Owner);
- targetDevoured.Recent = true;
- Dirty(args.Devoured, targetDevoured);
- Dirty(ent);
+ if (args.GrantedDna && TryGetDataFromOriginal(ent.AsNullable(), args.Devoured, out var data))
+ {
+ data.GrantedDna = true;
+ }
}
private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args)
@@ -75,6 +73,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
return;
data.Starting = true;
+ data.GrantedDna = true; // I have no idea how you're supposed to ever get DNA from yourself, but just in case.
ent.Comp.CurrentIdentity = data.Identity;
}
@@ -100,14 +99,13 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
}
}
- private void OnDevouredMobState(Entity ent, ref MobStateChangedEvent args)
+ private void OnRecentlyDevouredMobState(Entity ent, ref MobStateChangedEvent args)
{
- // Once we are revived the body is no longer "recent".
+ // Once we are revived the body is no longer recently devoured.
if (args.NewMobState != MobState.Alive)
return;
- ent.Comp.Recent = false;
- Dirty(ent);
+ RemCompDeferred(ent);
}
private void OnStoredRemove(Entity ent, ref ComponentRemove args)
@@ -222,6 +220,17 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
return clone;
}
+ ///
+ /// Marks that the changeling has successfully devoured the target.
+ ///
+ public void AddDevouredReference(Entity ent, EntityUid target)
+ {
+ var targetDevoured = EnsureComp(target);
+ targetDevoured.DevouredBy.Add(ent.Owner);
+
+ Dirty(target, targetDevoured);
+ }
+
///
/// Drop a stored identity from the changeling's storage.
///
@@ -348,10 +357,10 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
{
data.Identity = identity;
data.Original = original;
- data.OriginalName = Name(identity);
+ data.OriginalName = Name(original);
var foundMind = _mind.TryGetMind(original, out var mindId, out _);
- data.OriginalMind = mindId;
+ data.OriginalMind = foundMind ? mindId : null;
if (foundMind)
{