--- /dev/null
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Limits the number of traitors that can have the same objective.
+/// Checked by the prototype id, so only considers the exact same objectives.
+/// </summary>
+/// <remarks>
+/// Only works for traitors so don't use for anything else.
+/// </remarks>
+[RegisterComponent, Access(typeof(ObjectiveLimitSystem))]
+public sealed partial class ObjectiveLimitComponent : Component
+{
+ /// <summary>
+ /// Max number of players
+ /// </summary>
+ [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
+ public uint Limit;
+}
--- /dev/null
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Objectives.Components;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+
+public sealed class ObjectiveLimitSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<ObjectiveLimitComponent, RequirementCheckEvent>(OnCheck);
+ }
+
+ private void OnCheck(Entity<ObjectiveLimitComponent> ent, ref RequirementCheckEvent args)
+ {
+ if (args.Cancelled)
+ return;
+
+ if (Prototype(ent)?.ID is not {} proto)
+ {
+ Log.Error($"ObjectiveLimit used for non-prototyped objective {ent}");
+ return;
+ }
+
+ var remaining = ent.Comp.Limit;
+ // all traitor rules are considered
+ // maybe this would interfere with multistation stuff in the future but eh
+ foreach (var rule in EntityQuery<TraitorRuleComponent>())
+ {
+ foreach (var mindId in rule.TraitorMinds)
+ {
+ if (mindId == args.MindId || !HasObjective(mindId, proto))
+ continue;
+
+ remaining--;
+
+ // limit has been reached, prevent adding the objective
+ if (remaining == 0)
+ {
+ args.Cancelled = true;
+ return;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns true if the mind has an objective of a certain prototype.
+ /// </summary>
+ public bool HasObjective(EntityUid mindId, string proto, MindComponent? mind = null)
+ {
+ if (!Resolve(mindId, ref mind))
+ return false;
+
+ foreach (var objective in mind.AllObjectives)
+ {
+ if (Prototype(objective)?.ID == proto)
+ return true;
+ }
+
+ return false;
+ }
+}
verifyMapExistance: false
- type: Objective
difficulty: 2.75
+ - type: ObjectiveLimit
+ limit: 2 # there is usually only 1 of each steal objective, have 2 max for drama
# state
components:
- type: NotJobRequirement
job: HeadOfPersonnel
+ - type: ObjectiveLimit
+ limit: 3 # ian only has 2 slices, 3 obj for drama
- type: StealCondition
stealGroup: FoodMeatCorgi
owner: objective-condition-steal-Ian