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
This commit is contained in:
ScarKy0 2026-08-03 04:30:28 +02:00 • committed by GitHub
parent 1951b00875
commit b65eb25098
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 142 additions and 37 deletions

View file

@ -0,0 +1,10 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that a changeling has obtained the highest amount of unique devours out of all other changelings.
/// Checks against identities stored on the mind.
/// </summary>
[RegisterComponent, Access(typeof(ChangelingObjectiveSystem))]
public sealed partial class ChangelingDevourMostConditionComponent : Component;

View file

@ -3,15 +3,8 @@ using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// 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 <see cref="NumberObjectiveComponent"/> to function.
/// </summary>
[RegisterComponent, Access(typeof(ChangelingObjectiveSystem))]
public sealed partial class ChangelingUniqueIdentityConditionComponent : Component
{
/// <summary>
/// The amount of identities that have been already devoured.
/// </summary>
[DataField]
public int UniqueIdentities;
}
public sealed partial class ChangelingUniqueIdentityConditionComponent : Component;

View file

@ -122,6 +122,10 @@ public sealed partial class ObjectivesSystem : SharedObjectivesSystem
}
}
/// <summary>
/// Generates a summary for a list of antag minds based on their agent.
/// Contains the objective issuer, objectives and completion rate.
/// </summary>
private void AddSummary(StringBuilder result, string agent, List<(EntityUid, string)> minds)
{
var agentSummaries = new List<(string summary, float successRate, int completedObjectives)>();

View file

@ -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<ChangelingUniqueIdentityConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
SubscribeLocalEvent<ChangelingDevouredEvent>(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<ChangelingUniqueIdentityConditionComponent>(args.Changeling, out var obj))
return;
obj.UniqueIdentities++;
EnsureComp<ChangelingMindIdentityTrackerComponent>(mind, out var tracker);
tracker.Devoured++;
}
private void OnGetProgress(Entity<ChangelingUniqueIdentityConditionComponent> ent, ref ObjectiveGetProgressEvent args)
[SubscribeLocalEvent]
private void OnGetUniqueIdentitiesProgress(Entity<ChangelingUniqueIdentityConditionComponent> 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<ChangelingDevourMostConditionComponent> ent, ref ObjectiveGetProgressEvent args)
{
args.Progress = GetMostIdentitiesProgress(args.MindId);
}
/// <summary>
/// Returns the progress for <see cref="ChangelingUniqueIdentityConditionComponent"/>.
/// Uses data stored on the mind.
/// </summary>
/// <returns>Objective progress, between 0 and 1.</returns>
private float GetUniqueIdentitiesProgress(EntityUid mind, int target)
{
// We've never actually gained an identity.
if (!TryComp<ChangelingMindIdentityTrackerComponent>(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;
}
/// <summary>
/// Returns the progress for <see cref="ChangelingDevourMostConditionComponent"/> for a given mind.
/// Uses data stored on the mind.
/// </summary>
/// <returns>Objective progress, between 0 and 1.</returns>
private float GetMostIdentitiesProgress(EntityUid mind)
{
// Can't progress if we've never eaten anyone.
if (!TryComp<ChangelingMindIdentityTrackerComponent>(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<ChangelingMindIdentityTrackerComponent>();
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);
}
}

View file

@ -154,3 +154,13 @@ public sealed partial class ChangelingNetworkedIdentityData
[DataField]
public bool GrantedDna;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="Changeling">The changeling that gained an identity.</param>
/// <param name="Identity">The identity data that was obtained.</param>
/// <param name="NewIdentity">Whether this is the first time an identity was gained.</param>
[ByRefEvent]
public record struct ChangelingGainedOrUpdatedIdentityEvent(EntityUid Changeling, ChangelingIdentityData Identity, bool NewIdentity);

View file

@ -0,0 +1,16 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Changeling.Components;
/// <summary>
/// Tracks devoured identities on the mind of the changeling, used for objectives.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ChangelingMindIdentityTrackerComponent : Component
{
/// <summary>
/// Amount of unique entities devoured by this changeling.
/// </summary>
[DataField]
public int Devoured;
}

View file

@ -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
/// </summary>
/// <param name="ent">The Changeling.</param>
/// <param name="target">The target to clone.</param>
public EntityUid? GrantIdentity(Entity<ChangelingIdentityComponent?> ent, EntityUid target)
/// <param name="devoured">Whether this identity was granted via devouring, aka granted DNA to the changeling.</param>
public EntityUid? GrantIdentity(Entity<ChangelingIdentityComponent?> 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);

View file

@ -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;

View file

@ -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

View file

@ -146,7 +146,7 @@
id: ChangelingObjectiveGroupDevour
weights:
ChangelingUniqueIdentityObjective: 1
# eventually: devour most
ChangelingDevourMostObjective: 1
- type: weightedRandom
id: ChangelingObjectiveGroupEscapeDummy