diff --git a/Content.Server/Objectives/Components/ChangelingDevourMostConditionComponent.cs b/Content.Server/Objectives/Components/ChangelingDevourMostConditionComponent.cs
new file mode 100644
index 0000000000..75edde4505
--- /dev/null
+++ b/Content.Server/Objectives/Components/ChangelingDevourMostConditionComponent.cs
@@ -0,0 +1,10 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+///
+/// Requires that a changeling has obtained the highest amount of unique devours out of all other changelings.
+/// Checks against identities stored on the mind.
+///
+[RegisterComponent, Access(typeof(ChangelingObjectiveSystem))]
+public sealed partial class ChangelingDevourMostConditionComponent : Component;
diff --git a/Content.Server/Objectives/Components/ChangelingUniqueIdentityConditionComponent.cs b/Content.Server/Objectives/Components/ChangelingUniqueIdentityConditionComponent.cs
index 7e76d3a02a..97d8b7bb00 100644
--- a/Content.Server/Objectives/Components/ChangelingUniqueIdentityConditionComponent.cs
+++ b/Content.Server/Objectives/Components/ChangelingUniqueIdentityConditionComponent.cs
@@ -3,15 +3,8 @@ using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
///
-/// Requires that a changeling has obtained X unique identities.
+/// Requires that a changeling has obtained X unique identities. Checks against identities stored on the mind.
/// Depends on to function.
///
[RegisterComponent, Access(typeof(ChangelingObjectiveSystem))]
-public sealed partial class ChangelingUniqueIdentityConditionComponent : Component
-{
- ///
- /// The amount of identities that have been already devoured.
- ///
- [DataField]
- public int UniqueIdentities;
-}
+public sealed partial class ChangelingUniqueIdentityConditionComponent : Component;
diff --git a/Content.Server/Objectives/ObjectivesSystem.cs b/Content.Server/Objectives/ObjectivesSystem.cs
index c2efbd8e4e..f01b2cc30b 100644
--- a/Content.Server/Objectives/ObjectivesSystem.cs
+++ b/Content.Server/Objectives/ObjectivesSystem.cs
@@ -122,6 +122,10 @@ public sealed partial class ObjectivesSystem : SharedObjectivesSystem
}
}
+ ///
+ /// Generates a summary for a list of antag minds based on their agent.
+ /// Contains the objective issuer, objectives and completion rate.
+ ///
private void AddSummary(StringBuilder result, string agent, List<(EntityUid, string)> minds)
{
var agentSummaries = new List<(string summary, float successRate, int completedObjectives)>();
diff --git a/Content.Server/Objectives/Systems/ChangelingObjectiveSystem.cs b/Content.Server/Objectives/Systems/ChangelingObjectiveSystem.cs
index 8222f6d12f..b6f90e62c7 100644
--- a/Content.Server/Objectives/Systems/ChangelingObjectiveSystem.cs
+++ b/Content.Server/Objectives/Systems/ChangelingObjectiveSystem.cs
@@ -10,42 +10,89 @@ public sealed partial class ChangelingObjectiveSystem : EntitySystem
[Dependency] private NumberObjectiveSystem _number = default!;
[Dependency] private SharedMindSystem _mind = default!;
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnGetProgress);
- SubscribeLocalEvent(OnChangelingDevoured);
- }
-
+ [SubscribeLocalEvent]
private void OnChangelingDevoured(ref ChangelingDevouredEvent args)
{
- // We check if the devour granted us Dna.
- // We do this because we could already have gotten the identity by other means before, such as the Dna sting.
- // Dna grant basically ensures this is the first time valid devour has happened on a target.
+ if (!_mind.TryGetMind(args.Changeling, out var mind, out _))
+ return;
+
if (!args.GrantedDna)
return;
- if (!_mind.TryGetObjectiveComp(args.Changeling, out var obj))
- return;
-
- obj.UniqueIdentities++;
+ EnsureComp(mind, out var tracker);
+ tracker.Devoured++;
}
- private void OnGetProgress(Entity ent, ref ObjectiveGetProgressEvent args)
+ [SubscribeLocalEvent]
+ private void OnGetUniqueIdentitiesProgress(Entity ent, ref ObjectiveGetProgressEvent args)
{
- args.Progress = GetProgress(ent.Comp, _number.GetTarget(ent));
+ args.Progress = GetUniqueIdentitiesProgress(args.MindId, _number.GetTarget(ent));
}
- private float GetProgress(ChangelingUniqueIdentityConditionComponent comp, int target)
+ [SubscribeLocalEvent]
+ private void OnGetMostIdentitiesProgress(Entity ent, ref ObjectiveGetProgressEvent args)
{
+ args.Progress = GetMostIdentitiesProgress(args.MindId);
+ }
+
+ ///
+ /// Returns the progress for .
+ /// Uses data stored on the mind.
+ ///
+ /// Objective progress, between 0 and 1.
+ private float GetUniqueIdentitiesProgress(EntityUid mind, int target)
+ {
+ // We've never actually gained an identity.
+ if (!TryComp(mind, out var tracker))
+ return 0f;
+
// prevent divide-by-zero
if (target == 0)
return 1f;
- if (comp.UniqueIdentities >= target)
+ var uniqueCount = tracker.Devoured;
+
+ if (uniqueCount >= target)
return 1f;
- return (float)comp.UniqueIdentities / (float)target;
+ return (float)uniqueCount / (float)target;
+ }
+
+ ///
+ /// Returns the progress for for a given mind.
+ /// Uses data stored on the mind.
+ ///
+ /// Objective progress, between 0 and 1.
+ private float GetMostIdentitiesProgress(EntityUid mind)
+ {
+ // Can't progress if we've never eaten anyone.
+ if (!TryComp(mind, out var selfTracker))
+ return 0f;
+
+ // We never actually devoured anyone.
+ // We don't want to grant greentext if 0 is technically the highest.
+ if (selfTracker.Devoured is var selfUniqueCount && selfUniqueCount < 1)
+ return 0f;
+
+ var query = AllEntityQuery();
+
+ var highest = 0;
+
+ while (query.MoveNext(out var uid, out var tracker))
+ {
+ // Skip our own tracker. We only care for the highest others have.
+ if (uid == mind)
+ continue;
+
+ if (tracker.Devoured > highest)
+ highest = tracker.Devoured;
+ }
+
+ // No equal check. Only one can win.
+ if (selfUniqueCount > highest)
+ return 1f;
+
+ // highest+1 because we aim to be 1 above the highest.
+ return (float)selfUniqueCount / (float)(highest+1);
}
}
diff --git a/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs b/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs
index 110db2503e..93966692a9 100644
--- a/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs
+++ b/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs
@@ -154,3 +154,13 @@ public sealed partial class ChangelingNetworkedIdentityData
[DataField]
public bool GrantedDna;
}
+
+///
+/// Event raised on the changeling and broadcast when it gains a new identity, or an existing identity data is updated when gaining an identity.
+/// Identities are never removed, so any future grants of an identity (after dropping it, for example) is an update.
+///
+/// The changeling that gained an identity.
+/// The identity data that was obtained.
+/// Whether this is the first time an identity was gained.
+[ByRefEvent]
+public record struct ChangelingGainedOrUpdatedIdentityEvent(EntityUid Changeling, ChangelingIdentityData Identity, bool NewIdentity);
diff --git a/Content.Shared/Changeling/Components/ChangelingMindIdentityTrackerComponent.cs b/Content.Shared/Changeling/Components/ChangelingMindIdentityTrackerComponent.cs
new file mode 100644
index 0000000000..b1dea98574
--- /dev/null
+++ b/Content.Shared/Changeling/Components/ChangelingMindIdentityTrackerComponent.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Changeling.Components;
+
+///
+/// Tracks devoured identities on the mind of the changeling, used for objectives.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class ChangelingMindIdentityTrackerComponent : Component
+{
+ ///
+ /// Amount of unique entities devoured by this changeling.
+ ///
+ [DataField]
+ public int Devoured;
+}
diff --git a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
index 35099df73f..11662c8ac2 100644
--- a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
+++ b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
@@ -1,4 +1,4 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Changeling.Components;
using Content.Shared.Cloning;
@@ -44,7 +44,7 @@ public abstract partial class SharedChangelingIdentitySystem : EntitySystem
{
if (args.ObtainedIdentity)
{
- GrantIdentity(ent.Owner, args.Devoured);
+ GrantIdentity(ent.Owner, args.Devoured, true);
}
if (args.GrantedDna && TryGetDataFromOriginal(ent.AsNullable(), args.Devoured, out var data))
@@ -71,9 +71,6 @@ public abstract partial class SharedChangelingIdentitySystem : EntitySystem
if (!TryGetDataFromOriginal(ent.AsNullable(), ent, out var data))
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;
}
@@ -196,7 +193,8 @@ public abstract partial class SharedChangelingIdentitySystem : EntitySystem
///
/// The Changeling.
/// The target to clone.
- public EntityUid? GrantIdentity(Entity ent, EntityUid target)
+ /// Whether this identity was granted via devouring, aka granted DNA to the changeling.
+ public EntityUid? GrantIdentity(Entity ent, EntityUid target, bool devoured = false)
{
if (!Resolve(ent.Owner, ref ent.Comp))
return null;
@@ -206,17 +204,32 @@ public abstract partial class SharedChangelingIdentitySystem : EntitySystem
if (clone == null)
return null;
+ var isNew = false;
+
// We see if we already have a identity slot for this entity.
// This can happen if we devoured them before, but then dropped their stored identity.
if (!TryGetDataFromOriginal(ent.AsNullable(), target, out var newIdentity))
{
newIdentity = new ChangelingIdentityData();
ent.Comp.ConsumedIdentities.Add(newIdentity);
+ isNew = true; // Data didn't exist before so its not an update.
}
UpdateIdentityData(newIdentity, clone.Value, target);
AddDevouredReference(ent!, target);
+ newIdentity.GrantedDna = devoured;
+
+ if (newIdentity.Original == ent)
+ {
+ // This is an identity of us, we don't wanna count it for objectives n stuff.
+ newIdentity.Starting = true;
+ newIdentity.GrantedDna = true;
+ }
+
+ var ev = new ChangelingGainedOrUpdatedIdentityEvent(ent, newIdentity, isNew);
+ RaiseLocalEvent(ent, ref ev, true); // Broadcast it to allow the mind tracker to update.
+
HandlePvsOverride(ent, clone.Value);
Dirty(ent);
diff --git a/Content.Shared/Objectives/Components/ObjectiveComponent.cs b/Content.Shared/Objectives/Components/ObjectiveComponent.cs
index feb2c444ef..902bf0a813 100644
--- a/Content.Shared/Objectives/Components/ObjectiveComponent.cs
+++ b/Content.Shared/Objectives/Components/ObjectiveComponent.cs
@@ -1,5 +1,4 @@
using Content.Shared.Mind;
-using Content.Shared.Objectives;
using Content.Shared.Objectives.Prototypes;
using Content.Shared.Objectives.Systems;
using Robust.Shared.Utility;
diff --git a/Resources/Prototypes/Objectives/changeling.yml b/Resources/Prototypes/Objectives/changeling.yml
index 3a16db19ef..a2a9cc3b2b 100644
--- a/Resources/Prototypes/Objectives/changeling.yml
+++ b/Resources/Prototypes/Objectives/changeling.yml
@@ -80,3 +80,16 @@
- type: KillPersonCondition
requireDead: true
requireMaroon: true
+
+- type: entity
+ parent: BaseChangelingObjective
+ id: ChangelingDevourMostObjective
+ name: Devour the most humanoids.
+ description: We must devour the most unique humanoids out of all other changelings.
+ components:
+ - type: Objective
+ difficulty: 1
+ icon:
+ sprite: Mobs/Aliens/flesh.rsi
+ state: golem
+ - type: ChangelingDevourMostCondition
diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml
index bbe1b56794..9ecc2cb8c5 100644
--- a/Resources/Prototypes/Objectives/objectiveGroups.yml
+++ b/Resources/Prototypes/Objectives/objectiveGroups.yml
@@ -146,7 +146,7 @@
id: ChangelingObjectiveGroupDevour
weights:
ChangelingUniqueIdentityObjective: 1
- # eventually: devour most
+ ChangelingDevourMostObjective: 1
- type: weightedRandom
id: ChangelingObjectiveGroupEscapeDummy