From b65eb2509895d5bbcc03e2c75c67b0df83602ecb Mon Sep 17 00:00:00 2001 From: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Date: Mon, 3 Aug 2026 04:30:28 +0200 Subject: [PATCH] Changeling Devour Most Objective (#44169) * loving it * wording ig * first pass * makes more sense * Update changeling.yml * issuers to prototypes * *screams!!!!!!!!!!!!! * functional * yes im salty about the order * works! * screw this text * add it to rolls * nah * review * partial review * no more roundend text * remove append stuff * guh * var --- .../ChangelingDevourMostConditionComponent.cs | 10 +++ ...ngelingUniqueIdentityConditionComponent.cs | 11 +-- Content.Server/Objectives/ObjectivesSystem.cs | 4 + .../Systems/ChangelingObjectiveSystem.cs | 87 ++++++++++++++----- .../Components/ChangelingIdentityComponent.cs | 10 +++ .../ChangelingMindIdentityTrackerComponent.cs | 16 ++++ .../Systems/SharedChangelingIdentitySystem.cs | 25 ++++-- .../Components/ObjectiveComponent.cs | 1 - .../Prototypes/Objectives/changeling.yml | 13 +++ .../Prototypes/Objectives/objectiveGroups.yml | 2 +- 10 files changed, 142 insertions(+), 37 deletions(-) create mode 100644 Content.Server/Objectives/Components/ChangelingDevourMostConditionComponent.cs create mode 100644 Content.Shared/Changeling/Components/ChangelingMindIdentityTrackerComponent.cs 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