]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Objectives ecs rework (#19967)
authordeltanedas <39013340+deltanedas@users.noreply.github.com>
Sat, 16 Sep 2023 06:18:10 +0000 (07:18 +0100)
committerGitHub <noreply@github.com>
Sat, 16 Sep 2023 06:18:10 +0000 (16:18 +1000)
Co-authored-by: deltanedas <@deltanedas:kde.org>
106 files changed:
Content.Client/CharacterInfo/CharacterInfoSystem.cs
Content.Client/Objectives/Systems/ObjectivesSystem.cs [new file with mode: 0644]
Content.Client/UserInterface/Systems/Character/CharacterUIController.cs
Content.Server/CharacterInfo/CharacterInfoSystem.cs
Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs
Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
Content.Server/Ninja/Systems/SpaceNinjaSystem.cs
Content.Server/Objectives/Commands/AddObjectiveCommand.cs
Content.Server/Objectives/Commands/ListObjectivesCommand.cs
Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs
Content.Server/Objectives/Components/DieConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/DoorjackConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/EscapeShuttleConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/HelpProgressConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/KeepAliveConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/KillPersonConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/MultipleTraitorsRequirementComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/NotCommandRequirementComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/NotJobRequirementComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/NumberObjectiveComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/ObjectiveBlacklistRequirementComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/PickRandomHeadComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/PickRandomPersonComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/RandomTraitorAliveComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/RandomTraitorProgressComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/RoleRequirementComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/SocialObjectiveComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/SpiderChargeConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/SpiderChargeTargetRequirementComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/StealConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/StealResearchConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/SurviveConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/TargetObjectiveComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Components/TerrorConditionComponent.cs [new file with mode: 0644]
Content.Server/Objectives/Conditions/DieCondition.cs [deleted file]
Content.Server/Objectives/Conditions/DoorjackCondition.cs [deleted file]
Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs [deleted file]
Content.Server/Objectives/Conditions/KillPersonCondition.cs [deleted file]
Content.Server/Objectives/Conditions/KillRandomHeadCondition.cs [deleted file]
Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs [deleted file]
Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs [deleted file]
Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs [deleted file]
Content.Server/Objectives/Conditions/SpiderChargeCondition.cs [deleted file]
Content.Server/Objectives/Conditions/StealCondition.cs [deleted file]
Content.Server/Objectives/Conditions/StealResearchCondition.cs [deleted file]
Content.Server/Objectives/Conditions/SurviveCondition.cs [deleted file]
Content.Server/Objectives/Conditions/TerrorCondition.cs [deleted file]
Content.Server/Objectives/ObjectivesSystem.cs
Content.Server/Objectives/Requirements/IncompatibleConditionsRequirement.cs [deleted file]
Content.Server/Objectives/Requirements/IncompatibleObjectivesRequirement.cs [deleted file]
Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs [deleted file]
Content.Server/Objectives/Requirements/NinjaRequirement.cs [deleted file]
Content.Server/Objectives/Requirements/NotRoleRequirement.cs [deleted file]
Content.Server/Objectives/Requirements/SpiderChargeTargetRequirement.cs [deleted file]
Content.Server/Objectives/Requirements/TraitorRequirement.cs [deleted file]
Content.Server/Objectives/Systems/DieConditionSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/EscapeShuttleConditionSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/HelpProgressConditionSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/KeepAliveCondition.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/KillPersonConditionSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/MultipleTraitorsRequirementSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/NinjaConditionsSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/NotCommandRequirementSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/NotJobRequirementSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/NumberObjectiveSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/RoleRequirementSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/SpiderChargeTargetRequirementSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/StealConditionSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/SurviveConditionSystem.cs [new file with mode: 0644]
Content.Server/Objectives/Systems/TargetObjectiveSystem.cs [new file with mode: 0644]
Content.Shared/CharacterInfo/SharedCharacterInfoSystem.cs
Content.Shared/Mind/MindComponent.cs
Content.Shared/Mind/SharedMindSystem.cs
Content.Shared/Objectives/Components/ObjectiveComponent.cs [new file with mode: 0644]
Content.Shared/Objectives/ConditionInfo.cs [deleted file]
Content.Shared/Objectives/Interfaces/IObjectiveCondition.cs [deleted file]
Content.Shared/Objectives/Interfaces/IObjectiveRequirement.cs [deleted file]
Content.Shared/Objectives/Objective.cs [deleted file]
Content.Shared/Objectives/ObjectiveInfo.cs [new file with mode: 0644]
Content.Shared/Objectives/ObjectivePrototype.cs [deleted file]
Content.Shared/Objectives/Systems/SharedObjectivesSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/objectives/conditions/die-condition.ftl [deleted file]
Resources/Locale/en-US/objectives/conditions/doorjack.ftl [moved from Resources/Locale/en-US/objectives/conditions/doorjack-condition.ftl with 73% similarity]
Resources/Locale/en-US/objectives/conditions/escape-shuttle-condition.ftl [deleted file]
Resources/Locale/en-US/objectives/conditions/kill-head-condition.ftl [deleted file]
Resources/Locale/en-US/objectives/conditions/kill-head.ftl [new file with mode: 0644]
Resources/Locale/en-US/objectives/conditions/kill-person-condition.ftl [deleted file]
Resources/Locale/en-US/objectives/conditions/kill-person.ftl [new file with mode: 0644]
Resources/Locale/en-US/objectives/conditions/other-traitor-alive.ftl [moved from Resources/Locale/en-US/objectives/conditions/other-traitor-alive-condition.ftl with 50% similarity]
Resources/Locale/en-US/objectives/conditions/other-traitor-progress.ftl [moved from Resources/Locale/en-US/objectives/conditions/other-traitor-progress-condition.ftl with 54% similarity]
Resources/Locale/en-US/objectives/conditions/spider-charge-condition.ftl [deleted file]
Resources/Locale/en-US/objectives/conditions/spider-charge.ftl [new file with mode: 0644]
Resources/Locale/en-US/objectives/conditions/steal-research-condition.ftl [deleted file]
Resources/Locale/en-US/objectives/conditions/steal-research.ftl [new file with mode: 0644]
Resources/Locale/en-US/objectives/conditions/steal.ftl [moved from Resources/Locale/en-US/objectives/conditions/steal-condition.ftl with 100% similarity]
Resources/Locale/en-US/objectives/conditions/survive-condition.ftl [deleted file]
Resources/Locale/en-US/objectives/conditions/terror-condition.ftl [deleted file]
Resources/Locale/en-US/objectives/round-end.ftl
Resources/Prototypes/GameRules/midround.yml
Resources/Prototypes/Objectives/base_objectives.yml [new file with mode: 0644]
Resources/Prototypes/Objectives/ninja.yml [new file with mode: 0644]
Resources/Prototypes/Objectives/ninjaObjectives.yml [deleted file]
Resources/Prototypes/Objectives/objectiveGroups.yml
Resources/Prototypes/Objectives/traitor.yml [new file with mode: 0644]
Resources/Prototypes/Objectives/traitorObjectives.yml [deleted file]

index cd96085d3142fdb1e8e35710ef99ff3297961f11..93bd86d140bb2739c5e2a5edf03ca6e67ecf4dcc 100644 (file)
@@ -59,7 +59,7 @@ public sealed class CharacterInfoSystem : EntitySystem
     public readonly record struct CharacterData(
         EntityUid Entity,
         string Job,
-        Dictionary<string, List<ConditionInfo>> Objectives,
+        Dictionary<string, List<ObjectiveInfo>> Objectives,
         string? Briefing,
         string EntityName
     );
diff --git a/Content.Client/Objectives/Systems/ObjectivesSystem.cs b/Content.Client/Objectives/Systems/ObjectivesSystem.cs
new file mode 100644 (file)
index 0000000..8631ff2
--- /dev/null
@@ -0,0 +1,7 @@
+using Content.Shared.Objectives.Systems;
+
+namespace Content.Client.Objectives.Systems;
+
+public sealed class ObjectivesSystem : SharedObjectivesSystem
+{
+}
index beb37029c640e8f33b02b3dffc8d04c18360c633..925b2dae4fd45da80b7d1be60fa56585a5fb71fa 100644 (file)
@@ -128,7 +128,7 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
             foreach (var condition in conditions)
             {
                 var conditionControl = new ObjectiveConditionsControl();
-                conditionControl.ProgressTexture.Texture = _sprite.Frame0(condition.SpriteSpecifier);
+                conditionControl.ProgressTexture.Texture = _sprite.Frame0(condition.Icon);
                 conditionControl.ProgressTexture.Progress = condition.Progress;
                 var titleMessage = new FormattedMessage();
                 var descriptionMessage = new FormattedMessage();
index 602f9e8af0f21b9c97a813fe0f66eb8cef662be9..df8718a022e34fa17f7a9521f80b256f4f690eab 100644 (file)
@@ -3,6 +3,8 @@ using Content.Server.Roles;
 using Content.Server.Roles.Jobs;
 using Content.Shared.CharacterInfo;
 using Content.Shared.Objectives;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Objectives.Systems;
 
 namespace Content.Server.CharacterInfo;
 
@@ -11,6 +13,7 @@ public sealed class CharacterInfoSystem : EntitySystem
     [Dependency] private readonly JobSystem _jobs = default!;
     [Dependency] private readonly MindSystem _minds = default!;
     [Dependency] private readonly RoleSystem _roles = default!;
+    [Dependency] private readonly SharedObjectivesSystem _objectives = default!;
 
     public override void Initialize()
     {
@@ -27,7 +30,7 @@ public sealed class CharacterInfoSystem : EntitySystem
 
         var entity = args.SenderSession.AttachedEntity.Value;
 
-        var conditions = new Dictionary<string, List<ConditionInfo>>();
+        var objectives = new Dictionary<string, List<ObjectiveInfo>>();
         var jobTitle = "No Profession";
         string? briefing = null;
         if (_minds.TryGetMind(entity, out var mindId, out var mind))
@@ -35,13 +38,15 @@ public sealed class CharacterInfoSystem : EntitySystem
             // Get objectives
             foreach (var objective in mind.AllObjectives)
             {
-                if (!conditions.ContainsKey(objective.Prototype.Issuer))
-                    conditions[objective.Prototype.Issuer] = new List<ConditionInfo>();
-                foreach (var condition in objective.Conditions)
-                {
-                    conditions[objective.Prototype.Issuer].Add(new ConditionInfo(condition.Title,
-                        condition.Description, condition.Icon, condition.Progress));
-                }
+                var info = _objectives.GetInfo(objective, mindId, mind);
+                if (info == null)
+                    continue;
+
+                // group objectives by their issuer
+                var issuer = Comp<ObjectiveComponent>(objective).Issuer;
+                if (!objectives.ContainsKey(issuer))
+                    objectives[issuer] = new List<ObjectiveInfo>();
+                objectives[issuer].Add(info.Value);
             }
 
             if (_jobs.MindTryGetJobName(mindId, out var jobName))
@@ -51,6 +56,6 @@ public sealed class CharacterInfoSystem : EntitySystem
             briefing = _roles.MindGetBriefing(mindId);
         }
 
-        RaiseNetworkEvent(new CharacterInfoEvent(GetNetEntity(entity), jobTitle, conditions, briefing), args.SenderSession);
+        RaiseNetworkEvent(new CharacterInfoEvent(GetNetEntity(entity), jobTitle, objectives, briefing), args.SenderSession);
     }
 }
index 62eee90d50739cc9e263a6c7be8c744b82303fca..e0789ab753b5f386743b1bd242152a3981ed052d 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Server.Ninja.Systems;
 using Content.Shared.Communications;
-using Content.Shared.Objectives;
 using Robust.Shared.Audio;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
@@ -18,9 +17,9 @@ public sealed partial class NinjaRuleComponent : Component
     public List<EntityUid> Minds = new();
 
     /// <summary>
-    /// List of objective prototype ids to add
+    /// List of objective entity prototypes to add
     /// </summary>
-    [DataField("objectives", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<ObjectivePrototype>))]
+    [DataField("objectives", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
     public List<string> Objectives = new();
 
     /// <summary>
index 210425526c207b0b983ae6a0333fa1b4759facc3..01317dbfc11b240c3603fa61ac3c48fadac9ca70 100644 (file)
@@ -12,6 +12,7 @@ using Content.Shared.CCVar;
 using Content.Shared.Dataset;
 using Content.Shared.Mind;
 using Content.Shared.Mobs.Systems;
+using Content.Shared.Objectives.Components;
 using Content.Shared.PDA;
 using Content.Shared.Preferences;
 using Content.Shared.Roles;
@@ -299,8 +300,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
                 if (objective == null)
                     continue;
 
-                if (_mindSystem.TryAddObjective(mindId, mind, objective))
-                    difficulty += objective.Difficulty;
+                _mindSystem.AddObjective(mindId, mind, objective.Value);
+                difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty;
             }
         }
 
index 4d856f7fb38184c50552d05a3223781273a4ebd9..68827f956c19490f5333ed9141a8d58e30c7b9b1 100644 (file)
@@ -229,7 +229,7 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
         // assign objectives - must happen after spider charge target so that the obj requirement works
         foreach (var objective in config.Objectives)
         {
-            if (!_mind.TryAddObjective(mindId, objective, mind))
+            if (!_mind.TryAddObjective(mindId, mind, objective))
             {
                 Log.Error($"Failed to add {objective} to ninja {mind.OwnedEntity.Value}");
             }
index f5519e5095728f60d512ad400a46b449f3c8975b..f15dbf370c8854bd0f3d1de9d433fa365a82dfe8 100644 (file)
@@ -1,7 +1,7 @@
 using Content.Server.Administration;
 using Content.Shared.Administration;
 using Content.Shared.Mind;
-using Content.Shared.Objectives;
+using Content.Shared.Objectives.Components;
 using Robust.Server.Player;
 using Robust.Shared.Console;
 using Robust.Shared.Prototypes;
@@ -39,16 +39,17 @@ namespace Content.Server.Objectives.Commands
             }
 
             if (!IoCManager.Resolve<IPrototypeManager>()
-                .TryIndex<ObjectivePrototype>(args[1], out var objectivePrototype))
+                .TryIndex<EntityPrototype>(args[1], out var proto) ||
+                !proto.TryGetComponent<ObjectiveComponent>(out _))
             {
-                shell.WriteLine($"Can't find matching ObjectivePrototype {objectivePrototype}");
+                shell.WriteLine($"Can't find matching objective prototype {args[1]}");
                 return;
             }
 
-            var mindSystem = _entityManager.System<SharedMindSystem>();
-            if (!mindSystem.TryAddObjective(mindId, mind, objectivePrototype))
+            if (!minds.TryAddObjective(mindId, mind, args[1]))
             {
-                shell.WriteLine("Objective requirements dont allow that objective to be added.");
+                // can fail for other reasons so dont pretend to be right
+                shell.WriteLine("Failed to add the objective. Maybe requirements dont allow that objective to be added.");
             }
         }
     }
index d1a7feb0ca6151e972f31e305f427cd52256d408..93dec3fa44d97b2709ae7a3045c8d99f933b64f9 100644 (file)
@@ -2,6 +2,7 @@ using System.Linq;
 using Content.Server.Administration;
 using Content.Shared.Administration;
 using Content.Shared.Mind;
+using Content.Shared.Objectives.Systems;
 using Robust.Server.Player;
 using Robust.Shared.Console;
 
@@ -25,7 +26,7 @@ namespace Content.Server.Objectives.Commands
             }
 
             var minds = _entities.System<SharedMindSystem>();
-            if (!minds.TryGetMind(player, out _, out var mind))
+            if (!minds.TryGetMind(player, out var mindId, out var mind))
             {
                 shell.WriteError(LocalizationManager.GetString("shell-target-entity-does-not-have-message", ("missing", "mind")));
                 return;
@@ -38,9 +39,20 @@ namespace Content.Server.Objectives.Commands
                 shell.WriteLine("None.");
             }
 
+            var objectivesSystem = _entities.System<SharedObjectivesSystem>();
             for (var i = 0; i < objectives.Count; i++)
             {
-                shell.WriteLine($"- [{i}] {objectives[i].Conditions[0].Title}");
+                var info = objectivesSystem.GetInfo(objectives[i], mindId, mind);
+                if (info == null)
+                {
+                    shell.WriteLine($"- [{i}] {objectives[i]} - INVALID");
+                }
+                else
+                {
+
+                    var progress = (int) (info.Value.Progress * 100f);
+                    shell.WriteLine($"- [{i}] {objectives[i]} ({info.Value.Title}) ({progress}%)");
+                }
             }
         }
 
index 26369822fda9f6cb0ea38233560a0125567a4345..b174ca94b6320871e1c2e577f3d5a3c419415081 100644 (file)
@@ -30,7 +30,7 @@ namespace Content.Server.Objectives.Commands
                 return;
             }
 
-            if (!minds.TryGetMind(session, out _, out var mind))
+            if (!minds.TryGetMind(session, out var mindId, out var mind))
             {
                 shell.WriteLine("Can't find the mind.");
                 return;
@@ -39,7 +39,7 @@ namespace Content.Server.Objectives.Commands
             if (int.TryParse(args[1], out var i))
             {
                 var mindSystem = _entityManager.System<SharedMindSystem>();
-                shell.WriteLine(mindSystem.TryRemoveObjective(mind, i)
+                shell.WriteLine(mindSystem.TryRemoveObjective(mindId, mind, i)
                     ? "Objective successfully removed!"
                     : "Objective removing failed. Maybe the index is out of bounds? Check lsobjectives!");
             }
diff --git a/Content.Server/Objectives/Components/DieConditionComponent.cs b/Content.Server/Objectives/Components/DieConditionComponent.cs
new file mode 100644 (file)
index 0000000..bd928ae
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that the player dies to be complete.
+/// </summary>
+[RegisterComponent, Access(typeof(DieConditionSystem))]
+public sealed partial class DieConditionComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/DoorjackConditionComponent.cs b/Content.Server/Objectives/Components/DoorjackConditionComponent.cs
new file mode 100644 (file)
index 0000000..714a70d
--- /dev/null
@@ -0,0 +1,12 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Objective condition that requires the player to be a ninja and have doorjacked at least a random number of airlocks.
+/// Requires <see cref="NumberObjectiveComponent"/> to function.
+/// </summary>
+[RegisterComponent, Access(typeof(NinjaConditionsSystem))]
+public sealed partial class DoorjackConditionComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/EscapeShuttleConditionComponent.cs b/Content.Server/Objectives/Components/EscapeShuttleConditionComponent.cs
new file mode 100644 (file)
index 0000000..df5c3d1
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that the player is on the emergency shuttle's grid when docking to CentCom.
+/// </summary>
+[RegisterComponent, Access(typeof(EscapeShuttleConditionSystem))]
+public sealed partial class EscapeShuttleConditionComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/HelpProgressConditionComponent.cs b/Content.Server/Objectives/Components/HelpProgressConditionComponent.cs
new file mode 100644 (file)
index 0000000..df159a4
--- /dev/null
@@ -0,0 +1,12 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that a target completes half of their objectives.
+/// Depends on <see cref="TargetObjectiveComponent"/> to function.
+/// </summary>
+[RegisterComponent, Access(typeof(HelpProgressConditionSystem))]
+public sealed partial class HelpProgressConditionComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/KeepAliveConditionComponent.cs b/Content.Server/Objectives/Components/KeepAliveConditionComponent.cs
new file mode 100644 (file)
index 0000000..4d287b9
--- /dev/null
@@ -0,0 +1,12 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that a target stays alive.
+/// Depends on <see cref="TargetObjectiveComponent"/> to function.
+/// </summary>
+[RegisterComponent, Access(typeof(KeepAliveConditionSystem))]
+public sealed partial class KeepAliveConditionComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/KillPersonConditionComponent.cs b/Content.Server/Objectives/Components/KillPersonConditionComponent.cs
new file mode 100644 (file)
index 0000000..7bbc42a
--- /dev/null
@@ -0,0 +1,17 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that a target dies or, if <see cref="RequireDead"/> is false, is not on the emergency shuttle.
+/// Depends on <see cref="TargetObjectiveComponent"/> to function.
+/// </summary>
+[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
+public sealed partial class KillPersonConditionComponent : Component
+{
+    /// <summary>
+    /// Whether the target must be truly dead, ignores missing evac.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public bool RequireDead = false;
+}
diff --git a/Content.Server/Objectives/Components/MultipleTraitorsRequirementComponent.cs b/Content.Server/Objectives/Components/MultipleTraitorsRequirementComponent.cs
new file mode 100644 (file)
index 0000000..cb8202d
--- /dev/null
@@ -0,0 +1,16 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that there are a certain number of other traitors alive for this objective to be given.
+/// </summary>
+[RegisterComponent, Access(typeof(MultipleTraitorsRequirementSystem))]
+public sealed partial class MultipleTraitorsRequirementComponent : Component
+{
+    /// <summary>
+    /// Number of traitors, excluding yourself, that have to exist.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public int Traitors = 2;
+}
diff --git a/Content.Server/Objectives/Components/NotCommandRequirementComponent.cs b/Content.Server/Objectives/Components/NotCommandRequirementComponent.cs
new file mode 100644 (file)
index 0000000..bc7520e
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that the player is not a member of command.
+/// </summary>
+[RegisterComponent, Access(typeof(NotCommandRequirementSystem))]
+public sealed partial class NotCommandRequirementComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/NotJobRequirementComponent.cs b/Content.Server/Objectives/Components/NotJobRequirementComponent.cs
new file mode 100644 (file)
index 0000000..6f6619d
--- /dev/null
@@ -0,0 +1,17 @@
+using Content.Server.Objectives.Systems;
+using Content.Shared.Roles;
+using Content.Shared.Roles.Jobs;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+/// <summary>
+/// Requires that the player not have a certain job to have this objective.
+/// </summary>
+[RegisterComponent, Access(typeof(NotJobRequirementSystem))]
+public sealed partial class NotJobRequirementComponent : Component
+{
+    /// <summary>
+    /// ID of the job to ban from having this objective.
+    /// </summary>
+    [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<JobPrototype>))]
+    public string Job = string.Empty;
+}
diff --git a/Content.Server/Objectives/Components/NumberObjectiveComponent.cs b/Content.Server/Objectives/Components/NumberObjectiveComponent.cs
new file mode 100644 (file)
index 0000000..d4f4508
--- /dev/null
@@ -0,0 +1,41 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Objective has a target number of something.
+/// When the objective is assigned it randomly picks this target from a minimum to a maximum.
+/// </summary>
+[RegisterComponent, Access(typeof(NumberObjectiveSystem))]
+public sealed partial class NumberObjectiveComponent : Component
+{
+    /// <summary>
+    /// Number to use in the objective condition.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public int Target;
+
+    /// <summary>
+    /// Minimum number for target to roll.
+    /// </summary>
+    [DataField(required: true)]
+    public int Min;
+
+    /// <summary>
+    /// Maximum number for target to roll.
+    /// </summary>
+    [DataField(required: true)]
+    public int Max;
+
+    /// <summary>
+    /// Optional title locale id, passed "count" with <see cref="Target"/>.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public string? Title;
+
+    /// <summary>
+    /// Optional description locale id, passed "count" with <see cref="Target"/>.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public string? Description;
+}
diff --git a/Content.Server/Objectives/Components/ObjectiveBlacklistRequirementComponent.cs b/Content.Server/Objectives/Components/ObjectiveBlacklistRequirementComponent.cs
new file mode 100644 (file)
index 0000000..02475af
--- /dev/null
@@ -0,0 +1,15 @@
+using Content.Server.Objectives.Systems;
+using Content.Shared.Whitelist;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that the objective entity has no blacklisted components.
+/// Lets you check for incompatible objectives.
+/// </summary>
+[RegisterComponent, Access(typeof(ObjectiveBlacklistRequirementSystem))]
+public sealed partial class ObjectiveBlacklistRequirementComponent : Component
+{
+    [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
+    public EntityWhitelist Blacklist = new();
+}
diff --git a/Content.Server/Objectives/Components/PickRandomHeadComponent.cs b/Content.Server/Objectives/Components/PickRandomHeadComponent.cs
new file mode 100644 (file)
index 0000000..c2f82fb
--- /dev/null
@@ -0,0 +1,12 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random head.
+/// If there are no heads it will fallback to any person.
+/// </summary>
+[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
+public sealed partial class PickRandomHeadComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/PickRandomPersonComponent.cs b/Content.Server/Objectives/Components/PickRandomPersonComponent.cs
new file mode 100644 (file)
index 0000000..4188b1d
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person.
+/// </summary>
+[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
+public sealed partial class PickRandomPersonComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/RandomTraitorAliveComponent.cs b/Content.Server/Objectives/Components/RandomTraitorAliveComponent.cs
new file mode 100644 (file)
index 0000000..fd37d0d
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Sets the target for <see cref="KeepAliveConditionComponent"/> to a random traitor.
+/// </summary>
+[RegisterComponent, Access(typeof(KeepAliveConditionSystem))]
+public sealed partial class RandomTraitorAliveComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/RandomTraitorProgressComponent.cs b/Content.Server/Objectives/Components/RandomTraitorProgressComponent.cs
new file mode 100644 (file)
index 0000000..c05ac0d
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Sets the target for <see cref="HelpProgressConditionComponent"/> to a random traitor.
+/// </summary>
+[RegisterComponent, Access(typeof(HelpProgressConditionSystem))]
+public sealed partial class RandomTraitorProgressComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/RoleRequirementComponent.cs b/Content.Server/Objectives/Components/RoleRequirementComponent.cs
new file mode 100644 (file)
index 0000000..86f8d7c
--- /dev/null
@@ -0,0 +1,15 @@
+using Content.Server.Objectives.Systems;
+using Content.Shared.Whitelist;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that the player's mind matches a whitelist.
+/// Typical use is checking for (antagonist) roles.
+/// </summary>
+[RegisterComponent, Access(typeof(RoleRequirementSystem))]
+public sealed partial class RoleRequirementComponent : Component
+{
+    [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
+    public EntityWhitelist Roles = new();
+}
diff --git a/Content.Server/Objectives/Components/SocialObjectiveComponent.cs b/Content.Server/Objectives/Components/SocialObjectiveComponent.cs
new file mode 100644 (file)
index 0000000..cd8b427
--- /dev/null
@@ -0,0 +1,9 @@
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Marker component for social objectives and kill objectives to be mutually exclusive.
+/// </summary>
+[RegisterComponent]
+public sealed partial class SocialObjectiveComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/SpiderChargeConditionComponent.cs b/Content.Server/Objectives/Components/SpiderChargeConditionComponent.cs
new file mode 100644 (file)
index 0000000..4fbe857
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that the player is a ninja and blew up their spider charge at its target location.
+/// </summary>
+[RegisterComponent, Access(typeof(NinjaConditionsSystem))]
+public sealed partial class SpiderChargeConditionComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/SpiderChargeTargetRequirementComponent.cs b/Content.Server/Objectives/Components/SpiderChargeTargetRequirementComponent.cs
new file mode 100644 (file)
index 0000000..e148d77
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires the player to be a ninja that has a spider charge target assigned, which is almost always the case.
+/// </summary>
+[RegisterComponent, Access(typeof(SpiderChargeTargetRequirementSystem))]
+public sealed partial class SpiderChargeTargetRequirementComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/StealConditionComponent.cs b/Content.Server/Objectives/Components/StealConditionComponent.cs
new file mode 100644 (file)
index 0000000..b52ac9c
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Server.Objectives.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that you steal a certain item.
+/// </summary>
+[RegisterComponent, Access(typeof(StealConditionSystem))]
+public sealed partial class StealConditionComponent : Component
+{
+    /// <summary>
+    /// The id of the item to steal.
+    /// </summary>
+    /// <remarks>
+    /// Works by prototype id not tags or anything so it has to be the exact item.
+    /// </remarks>
+    [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
+    public string Prototype = string.Empty;
+
+    /// <summary>
+    /// Help newer players by saying e.g. "steal the chief engineer's advanced magboots"
+    /// instead of "steal advanced magboots. Should be a loc string.
+    /// </summary>
+    [DataField("owner"), ViewVariables(VVAccess.ReadWrite)]
+    public string? OwnerText;
+}
diff --git a/Content.Server/Objectives/Components/StealResearchConditionComponent.cs b/Content.Server/Objectives/Components/StealResearchConditionComponent.cs
new file mode 100644 (file)
index 0000000..736a2e7
--- /dev/null
@@ -0,0 +1,12 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Objective condition that requires the player to be a ninja and have stolen at least a random number of technologies.
+/// Requires <see cref="NumberObjectiveComponent"/> to function.
+/// </summary>
+[RegisterComponent, Access(typeof(NinjaConditionsSystem))]
+public sealed partial class StealResearchConditionComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/SurviveConditionComponent.cs b/Content.Server/Objectives/Components/SurviveConditionComponent.cs
new file mode 100644 (file)
index 0000000..80c1cc8
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Just requires that the player is not dead, ignores evac and what not.
+/// </summary>
+[RegisterComponent, Access(typeof(SurviveConditionSystem))]
+public sealed partial class SurviveConditionComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Components/TargetObjectiveComponent.cs b/Content.Server/Objectives/Components/TargetObjectiveComponent.cs
new file mode 100644 (file)
index 0000000..c0cd521
--- /dev/null
@@ -0,0 +1,21 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+[RegisterComponent, Access(typeof(TargetObjectiveSystem))]
+public sealed partial class TargetObjectiveComponent : Component
+{
+    /// <summary>
+    /// Locale id for the objective title.
+    /// It is passed "targetName" and "job" arguments.
+    /// </summary>
+    [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
+    public string Title = string.Empty;
+
+    /// <summary>
+    /// Mind entity id of the target.
+    /// This must be set by another system using <see cref="TargetObjectiveSystem.SetTarget"/>.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public EntityUid? Target;
+}
diff --git a/Content.Server/Objectives/Components/TerrorConditionComponent.cs b/Content.Server/Objectives/Components/TerrorConditionComponent.cs
new file mode 100644 (file)
index 0000000..c94e3b4
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+/// <summary>
+/// Requires that the player is a ninja and has called in a threat.
+/// </summary>
+[RegisterComponent, Access(typeof(NinjaConditionsSystem))]
+public sealed partial class TerrorConditionComponent : Component
+{
+}
diff --git a/Content.Server/Objectives/Conditions/DieCondition.cs b/Content.Server/Objectives/Conditions/DieCondition.cs
deleted file mode 100644 (file)
index d5e9e1a..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using JetBrains.Annotations;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions
-{
-    [UsedImplicitly]
-    [DataDefinition]
-    public sealed partial class DieCondition : IObjectiveCondition
-    {
-        private MindComponent? _mind;
-
-        public IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
-        {
-            return new DieCondition { _mind = mind };
-        }
-
-        public string Title => Loc.GetString("objective-condition-die-title");
-
-        public string Description => Loc.GetString("objective-condition-die-description");
-
-        public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Mobs/Ghosts/ghost_human.rsi"), "icon");
-
-        public float Progress
-        {
-            get
-            {
-                var entityManager = IoCManager.Resolve<EntityManager>();
-                var mindSystem = entityManager.System<SharedMindSystem>();
-                return _mind == null || mindSystem.IsCharacterDeadIc(_mind) ? 1f : 0f;
-            }
-        }
-
-        public float Difficulty => 0.5f;
-
-        public bool Equals(IObjectiveCondition? other)
-        {
-            return other is DieCondition condition && Equals(_mind, condition._mind);
-        }
-
-        public override bool Equals(object? obj)
-        {
-            if (ReferenceEquals(null, obj)) return false;
-            if (ReferenceEquals(this, obj)) return true;
-            if (obj.GetType() != GetType()) return false;
-            return Equals((DieCondition) obj);
-        }
-
-        public override int GetHashCode()
-        {
-            return (_mind != null ? _mind.GetHashCode() : 0);
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/DoorjackCondition.cs b/Content.Server/Objectives/Conditions/DoorjackCondition.cs
deleted file mode 100644 (file)
index 0752048..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-using Content.Server.Roles;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using Robust.Shared.Random;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions;
-
-/// <summary>
-/// Objective condition that requires the player to be a ninja and have doorjacked at least a random number of airlocks.
-/// </summary>
-[DataDefinition]
-public sealed partial class DoorjackCondition : IObjectiveCondition
-{
-    private EntityUid? _mind;
-    private int _target;
-
-    public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
-    {
-        // TODO: clamp to number of doors on station incase its somehow a shittle or something
-        return new DoorjackCondition {
-            _mind = uid,
-            _target = IoCManager.Resolve<IRobustRandom>().Next(15, 40)
-        };
-    }
-
-    public string Title => Loc.GetString("objective-condition-doorjack-title", ("count", _target));
-
-    public string Description => Loc.GetString("objective-condition-doorjack-description", ("count", _target));
-
-    public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Tools/emag.rsi"), "icon");
-
-    public float Progress
-    {
-        get
-        {
-            // prevent divide-by-zero
-            if (_target == 0)
-                return 1f;
-
-            var entMan = IoCManager.Resolve<IEntityManager>();
-            if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
-                return 0f;
-
-            if (role.DoorsJacked >= _target)
-                return 1f;
-
-            return (float) role.DoorsJacked / (float) _target;
-        }
-    }
-
-    public float Difficulty => 1.5f;
-
-    public bool Equals(IObjectiveCondition? other)
-    {
-        return other is DoorjackCondition cond && Equals(_mind, cond._mind) && _target == cond._target;
-    }
-
-    public override bool Equals(object? obj)
-    {
-        if (ReferenceEquals(null, obj)) return false;
-        if (ReferenceEquals(this, obj)) return true;
-        return obj is DoorjackCondition cond && cond.Equals(this);
-    }
-
-    public override int GetHashCode()
-    {
-        return HashCode.Combine(_mind?.GetHashCode() ?? 0, _target);
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs b/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs
deleted file mode 100644 (file)
index 27d7975..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-using Content.Server.Shuttles.Systems;
-using Content.Shared.Cuffs.Components;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using JetBrains.Annotations;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions
-{
-    [UsedImplicitly]
-    [DataDefinition]
-    public sealed partial class EscapeShuttleCondition : IObjectiveCondition
-    {
-        // TODO refactor all of this to be ecs
-        private MindComponent? _mind;
-
-        public IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
-        {
-            return new EscapeShuttleCondition
-            {
-                _mind = mind,
-            };
-        }
-
-        public string Title => Loc.GetString("objective-condition-escape-shuttle-title");
-
-        public string Description => Loc.GetString("objective-condition-escape-shuttle-description");
-
-        public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Structures/Furniture/chairs.rsi"), "shuttle");
-
-        public float Progress
-        {
-            get {
-                var entMan = IoCManager.Resolve<IEntityManager>();
-                var mindSystem = entMan.System<SharedMindSystem>();
-
-                if (_mind?.OwnedEntity == null
-                    || !entMan.TryGetComponent<TransformComponent>(_mind.OwnedEntity, out var xform))
-                    return 0f;
-
-                if (mindSystem.IsCharacterDeadIc(_mind))
-                    return 0f;
-
-                if (entMan.TryGetComponent<CuffableComponent>(_mind.OwnedEntity, out var cuffed)
-                    && cuffed.CuffedHandCount > 0)
-                {
-                    // You're not escaping if you're restrained!
-                    return 0f;
-                }
-
-                // Any emergency shuttle counts for this objective, but not pods.
-                var emergencyShuttle = entMan.System<EmergencyShuttleSystem>();
-                if (!emergencyShuttle.IsTargetEscaping(_mind.OwnedEntity.Value))
-                    return 0f;
-
-                return 1f;
-            }
-        }
-
-        public float Difficulty => 1.3f;
-
-        public bool Equals(IObjectiveCondition? other)
-        {
-            return other is EscapeShuttleCondition esc && Equals(_mind, esc._mind);
-        }
-
-        public override bool Equals(object? obj)
-        {
-            if (ReferenceEquals(null, obj)) return false;
-            if (ReferenceEquals(this, obj)) return true;
-            if (obj.GetType() != GetType()) return false;
-            return Equals((EscapeShuttleCondition) obj);
-        }
-
-        public override int GetHashCode()
-        {
-            return _mind != null ? _mind.GetHashCode() : 0;
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/KillPersonCondition.cs b/Content.Server/Objectives/Conditions/KillPersonCondition.cs
deleted file mode 100644 (file)
index b5f16a6..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-using Content.Server.Shuttles.Systems;
-using Content.Shared.CCVar;
-using Content.Shared.Mind;
-using Content.Shared.Mobs.Systems;
-using Content.Shared.Objectives.Interfaces;
-using Content.Shared.Roles.Jobs;
-using Robust.Shared.Configuration;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions
-{
-    public abstract class KillPersonCondition : IObjectiveCondition
-    {
-        // TODO refactor all of this to be ecs
-        protected IEntityManager EntityManager => IoCManager.Resolve<IEntityManager>();
-        protected SharedMindSystem Minds => EntityManager.System<SharedMindSystem>();
-        protected SharedJobSystem Jobs => EntityManager.System<SharedJobSystem>();
-        protected MobStateSystem MobStateSystem => EntityManager.System<MobStateSystem>();
-        protected EntityUid? TargetMindId;
-        protected MindComponent? TargetMind => EntityManager.GetComponentOrNull<MindComponent>(TargetMindId);
-        public abstract IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind);
-
-        /// <summary>
-        /// Whether the target must be truly dead, ignores missing evac.
-        /// </summary>
-        protected bool RequireDead = false;
-
-        public string Title
-        {
-            get
-            {
-                var mind = TargetMind;
-                var targetName = mind?.CharacterName ?? "Unknown";
-                var jobName = Jobs.MindTryGetJobName(TargetMindId);
-
-                if (TargetMind == null)
-                    return Loc.GetString("objective-condition-kill-person-title", ("targetName", targetName), ("job", jobName));
-
-                return Loc.GetString("objective-condition-kill-person-title", ("targetName", targetName), ("job", jobName));
-            }
-        }
-
-        public string Description => Loc.GetString("objective-condition-kill-person-description");
-
-        public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Objects/Weapons/Guns/Pistols/viper.rsi"), "icon");
-
-        public float Progress
-        {
-            get
-            {
-                if (TargetMindId == null || TargetMind?.OwnedEntity == null)
-                    return 1f;
-
-                var entMan = IoCManager.Resolve<EntityManager>();
-                var mindSystem = entMan.System<SharedMindSystem>();
-                if (mindSystem.IsCharacterDeadIc(TargetMind))
-                    return 1f;
-
-                if (RequireDead)
-                    return 0f;
-
-                // if evac is disabled then they really do have to be dead
-                var configMan = IoCManager.Resolve<IConfigurationManager>();
-                if (!configMan.GetCVar(CCVars.EmergencyShuttleEnabled))
-                    return 0f;
-
-                // target is escaping so you fail
-                var emergencyShuttle = entMan.System<EmergencyShuttleSystem>();
-                if (emergencyShuttle.IsTargetEscaping(TargetMind.OwnedEntity.Value))
-                    return 0f;
-
-                // evac has left without the target, greentext since the target is afk in space with a full oxygen tank and coordinates off.
-                if (emergencyShuttle.ShuttlesLeft)
-                    return 1f;
-
-                // if evac is still here and target hasn't boarded, show 50% to give you an indicator that you are doing good
-                return emergencyShuttle.EmergencyShuttleArrived ? 0.5f : 0f;
-            }
-        }
-
-        public float Difficulty => 1.75f;
-
-        public bool Equals(IObjectiveCondition? other)
-        {
-            return other is KillPersonCondition kpc && Equals(TargetMindId, kpc.TargetMindId);
-        }
-
-        public override bool Equals(object? obj)
-        {
-            if (ReferenceEquals(null, obj)) return false;
-            if (ReferenceEquals(this, obj)) return true;
-            if (obj.GetType() != GetType()) return false;
-            return Equals((KillPersonCondition) obj);
-        }
-
-        public override int GetHashCode()
-        {
-            return TargetMindId?.GetHashCode() ?? 0;
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/KillRandomHeadCondition.cs b/Content.Server/Objectives/Conditions/KillRandomHeadCondition.cs
deleted file mode 100644 (file)
index 2703a37..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Linq;
-using Content.Shared.Mind;
-using Content.Shared.Mind.Components;
-using Content.Shared.Mobs.Components;
-using Content.Shared.Objectives.Interfaces;
-using Robust.Shared.Random;
-
-namespace Content.Server.Objectives.Conditions;
-
-[DataDefinition]
-public sealed partial class KillRandomHeadCondition : KillPersonCondition
-{
-    // TODO refactor all of this to be ecs
-    public override IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
-    {
-        RequireDead = true;
-
-        var allHumans = EntityManager.EntityQuery<MindContainerComponent>(true).Where(mc =>
-        {
-            var entity = EntityManagerExt.GetComponentOrNull<MindComponent>(EntityManager, (EntityUid?) mc.Mind)?.OwnedEntity;
-
-            if (entity == default)
-                return false;
-
-            return EntityManager.TryGetComponent(entity, out MobStateComponent? mobState) &&
-                   MobStateSystem.IsAlive(entity.Value, mobState) &&
-                   mc.Mind != mindId;
-        }).Select(mc => mc.Mind).ToList();
-
-        if (allHumans.Count == 0)
-            return new DieCondition(); // I guess I'll die
-
-        var allHeads = allHumans
-            .Where(mind => Jobs.MindTryGetJob(mind, out _, out var prototype) && prototype.RequireAdminNotify)
-            .ToList();
-
-        if (allHeads.Count == 0)
-            allHeads = allHumans; // fallback to non-head target
-
-        return new KillRandomHeadCondition { TargetMindId = IoCManager.Resolve<IRobustRandom>().Pick(allHeads) };
-    }
-
-    public string Description => Loc.GetString("objective-condition-kill-head-description");
-}
diff --git a/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs b/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs
deleted file mode 100644 (file)
index 74eb642..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-using Content.Shared.Humanoid;
-using Content.Shared.Mind;
-using Content.Shared.Mind.Components;
-using Content.Shared.Mobs.Components;
-using Content.Shared.Objectives.Interfaces;
-using Robust.Shared.Random;
-
-namespace Content.Server.Objectives.Conditions;
-
-[DataDefinition]
-public sealed partial class KillRandomPersonCondition : KillPersonCondition
-{
-    public override IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
-    {
-        var allHumans = new List<EntityUid>();
-        var query = EntityManager.EntityQuery<MindContainerComponent, HumanoidAppearanceComponent>(true);
-        foreach (var (mc, _) in query)
-        {
-            var entity = EntityManager.GetComponentOrNull<MindComponent>(mc.Mind)?.OwnedEntity;
-            if (entity == default)
-                continue;
-
-            if (EntityManager.TryGetComponent(entity, out MobStateComponent? mobState) &&
-                MobStateSystem.IsAlive(entity.Value, mobState) &&
-                mc.Mind != mindId && mc.Mind != null)
-            {
-                allHumans.Add(mc.Mind.Value);
-            }
-        }
-
-        if (allHumans.Count == 0)
-            return new DieCondition(); // I guess I'll die
-
-        return new KillRandomPersonCondition {TargetMindId = IoCManager.Resolve<IRobustRandom>().Pick(allHumans)};
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs b/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs
deleted file mode 100644 (file)
index 8f914f0..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-using System.Linq;
-using Content.Server.GameTicking.Rules;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using Content.Shared.Roles.Jobs;
-using Robust.Shared.Random;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions
-{
-    [DataDefinition]
-    public sealed partial class RandomTraitorAliveCondition : IObjectiveCondition
-    {
-        private EntityUid? _targetMind;
-
-        public IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
-        {
-            var entityMgr = IoCManager.Resolve<IEntityManager>();
-
-            var traitors = Enumerable.ToList<(EntityUid Id, MindComponent Mind)>(entityMgr.System<TraitorRuleSystem>().GetOtherTraitorMindsAliveAndConnected(mind));
-
-            if (traitors.Count == 0)
-                return new EscapeShuttleCondition(); //You were made a traitor by admins, and are the first/only.
-            return new RandomTraitorAliveCondition { _targetMind = IoCManager.Resolve<IRobustRandom>().Pick(traitors).Id };
-        }
-
-        public string Title
-        {
-            get
-            {
-                var targetName = string.Empty;
-                var ents = IoCManager.Resolve<IEntityManager>();
-                var jobs = ents.System<SharedJobSystem>();
-                var jobName = jobs.MindTryGetJobName(_targetMind);
-
-                if (_targetMind == null)
-                    return Loc.GetString("objective-condition-other-traitor-alive-title", ("targetName", targetName), ("job", jobName));
-
-                if (ents.TryGetComponent(_targetMind, out MindComponent? mind) &&
-                    mind.OwnedEntity is {Valid: true} owned)
-                {
-                    targetName = ents.GetComponent<MetaDataComponent>(owned).EntityName;
-                }
-
-                return Loc.GetString("objective-condition-other-traitor-alive-title", ("targetName", targetName), ("job", jobName));
-            }
-        }
-
-        public string Description => Loc.GetString("objective-condition-other-traitor-alive-description");
-
-        public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Objects/Misc/bureaucracy.rsi"), "folder-white");
-
-        public float Progress
-        {
-            get
-            {
-                var entityManager = IoCManager.Resolve<EntityManager>();
-                var mindSystem = entityManager.System<SharedMindSystem>();
-                return !entityManager.TryGetComponent(_targetMind, out MindComponent? mind) ||
-                       !mindSystem.IsCharacterDeadIc(mind)
-                    ? 1f
-                    : 0f;
-            }
-        }
-
-        public float Difficulty => 1.75f;
-
-        public bool Equals(IObjectiveCondition? other)
-        {
-            return other is RandomTraitorAliveCondition kpc && Equals(_targetMind, kpc._targetMind);
-        }
-
-        public override bool Equals(object? obj)
-        {
-            if (ReferenceEquals(null, obj)) return false;
-            if (ReferenceEquals(this, obj)) return true;
-            return obj is RandomTraitorAliveCondition alive && alive.Equals(this);
-        }
-
-        public override int GetHashCode()
-        {
-            return _targetMind?.GetHashCode() ?? 0;
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs b/Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs
deleted file mode 100644 (file)
index 4b0dc01..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-using System.Linq;
-using Content.Server.GameTicking.Rules;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using Content.Shared.Roles.Jobs;
-using Robust.Shared.Random;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions
-{
-    [DataDefinition]
-    public sealed partial class RandomTraitorProgressCondition : IObjectiveCondition
-    {
-        // TODO ecs all of this
-        private EntityUid? _targetMind;
-
-        public IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
-        {
-            //todo shit of a fuck
-            var entityMgr = IoCManager.Resolve<IEntityManager>();
-
-            var traitors = entityMgr.System<TraitorRuleSystem>().GetOtherTraitorMindsAliveAndConnected(mind).ToList();
-            List<EntityUid> removeList = new();
-
-            foreach (var traitor in traitors)
-            {
-                foreach (var objective in traitor.Mind.AllObjectives)
-                {
-                    foreach (var condition in objective.Conditions)
-                    {
-                        if (condition is RandomTraitorProgressCondition)
-                        {
-                            removeList.Add(traitor.Id);
-                        }
-                    }
-                }
-            }
-
-            foreach (var traitor in removeList)
-            {
-                traitors.RemoveAll(t => t.Id == traitor);
-            }
-
-            if (traitors.Count == 0) return new EscapeShuttleCondition{}; //You were made a traitor by admins, and are the first/only.
-            return new RandomTraitorProgressCondition { _targetMind = IoCManager.Resolve<IRobustRandom>().Pick(traitors).Id };
-        }
-
-        public string Title
-        {
-            get
-            {
-                var targetName = string.Empty;
-                var entities = IoCManager.Resolve<IEntityManager>();
-                var jobs = entities.System<SharedJobSystem>();
-                var jobName = jobs.MindTryGetJobName(_targetMind);
-
-                if (_targetMind == null)
-                    return Loc.GetString("objective-condition-other-traitor-progress-title", ("targetName", targetName), ("job", jobName));
-
-                if (entities.TryGetComponent(_targetMind, out MindComponent? mind) &&
-                    mind.OwnedEntity is {Valid: true} owned)
-                {
-                    targetName = entities.GetComponent<MetaDataComponent>(owned).EntityName;
-                }
-
-                return Loc.GetString("objective-condition-other-traitor-progress-title", ("targetName", targetName), ("job", jobName));
-            }
-        }
-
-        public string Description => Loc.GetString("objective-condition-other-traitor-progress-description");
-
-        public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Objects/Misc/bureaucracy.rsi"), "folder-white");
-
-        public float Progress
-        {
-            get
-            {
-                float total = 0f; // how much progress they have
-                float max = 0f; // how much progress is needed for 100%
-
-                if (_targetMind == null)
-                {
-                    Logger.Error("Null target on RandomTraitorProgressCondition.");
-                    return 1f;
-                }
-
-                var entities = IoCManager.Resolve<IEntityManager>();
-                if (entities.TryGetComponent(_targetMind, out MindComponent? mind))
-                {
-                    foreach (var objective in mind.AllObjectives)
-                    {
-                        foreach (var condition in objective.Conditions)
-                        {
-                            max++; // things can only be up to 100% complete yeah
-                            total += condition.Progress;
-                        }
-                    }
-                }
-
-                if (max == 0f)
-                {
-                    Logger.Error("RandomTraitorProgressCondition assigned someone with no objectives to be helped.");
-                    return 1f;
-                }
-
-                var completion = total / max;
-
-                if (completion >= 0.5f)
-                    return 1f;
-                else
-                    return completion / 0.5f;
-            }
-        }
-
-        public float Difficulty => 2.5f;
-
-        public bool Equals(IObjectiveCondition? other)
-        {
-            return other is RandomTraitorProgressCondition kpc && Equals(_targetMind, kpc._targetMind);
-        }
-
-        public override bool Equals(object? obj)
-        {
-            if (ReferenceEquals(null, obj)) return false;
-            if (ReferenceEquals(this, obj)) return true;
-            return obj is RandomTraitorProgressCondition alive && alive.Equals(this);
-        }
-
-        public override int GetHashCode()
-        {
-            return _targetMind?.GetHashCode() ?? 0;
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/SpiderChargeCondition.cs b/Content.Server/Objectives/Conditions/SpiderChargeCondition.cs
deleted file mode 100644 (file)
index 5209296..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-using Content.Server.Roles;
-using Content.Server.Warps;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using Robust.Shared.Random;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions;
-
-/// <summary>
-/// Objective condition that requires the player to be a ninja and have detonated their spider charge.
-/// </summary>
-[DataDefinition]
-public sealed partial class SpiderChargeCondition : IObjectiveCondition
-{
-    private EntityUid? _mind;
-
-    public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
-    {
-        return new SpiderChargeCondition {
-            _mind = uid
-        };
-    }
-
-    public string Title
-    {
-        get
-        {
-            var entMan = IoCManager.Resolve<IEntityManager>();
-            if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role)
-                || role.SpiderChargeTarget == null
-                || !entMan.TryGetComponent<WarpPointComponent>(role.SpiderChargeTarget, out var warp)
-                || warp.Location == null)
-                // this should never really happen but eh
-                return Loc.GetString("objective-condition-spider-charge-no-target");
-
-            return Loc.GetString("objective-condition-spider-charge-title", ("location", warp.Location));
-        }
-    }
-
-    public string Description => Loc.GetString("objective-condition-spider-charge-description");
-
-    public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Weapons/Bombs/spidercharge.rsi"), "icon");
-
-    public float Progress
-    {
-        get
-        {
-            var entMan = IoCManager.Resolve<EntityManager>();
-            if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
-                return 0f;
-
-            return role.SpiderChargeDetonated ? 1f : 0f;
-        }
-    }
-
-    public float Difficulty => 2.5f;
-
-    public bool Equals(IObjectiveCondition? other)
-    {
-        return other is SpiderChargeCondition cond && Equals(_mind, cond._mind);
-    }
-
-    public override bool Equals(object? obj)
-    {
-        if (ReferenceEquals(null, obj)) return false;
-        if (ReferenceEquals(this, obj)) return true;
-        return obj is SpiderChargeCondition cond && cond.Equals(this);
-    }
-
-    public override int GetHashCode()
-    {
-        return _mind?.GetHashCode() ?? 0;
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/StealCondition.cs b/Content.Server/Objectives/Conditions/StealCondition.cs
deleted file mode 100644 (file)
index 5ca36a7..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using JetBrains.Annotations;
-using Robust.Shared.Containers;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions
-{
-    // Oh god my eyes
-    [UsedImplicitly]
-    [DataDefinition]
-    public sealed partial class StealCondition : IObjectiveCondition, ISerializationHooks
-    {
-        private EntityUid? _mind;
-        [DataField("prototype")] private string _prototypeId = string.Empty;
-
-        /// <summary>
-        /// Help newer players by saying e.g. "steal the chief engineer's advanced magboots"
-        /// instead of "steal advanced magboots. Should be a loc string.
-        /// </summary>
-        [DataField("owner")] private string? _owner = null;
-
-        public IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
-        {
-            return new StealCondition
-            {
-                _mind = mindId,
-                _prototypeId = _prototypeId,
-                _owner = _owner
-            };
-        }
-
-        private string PrototypeName =>
-            IoCManager.Resolve<IPrototypeManager>().TryIndex<EntityPrototype>(_prototypeId, out var prototype)
-                ? prototype.Name
-                : "[CANNOT FIND NAME]";
-
-        public string Title =>
-            _owner == null
-                ? Loc.GetString("objective-condition-steal-title-no-owner", ("itemName", Loc.GetString(PrototypeName)))
-                : Loc.GetString("objective-condition-steal-title", ("owner", Loc.GetString(_owner)), ("itemName", Loc.GetString(PrototypeName)));
-
-        public string Description => Loc.GetString("objective-condition-steal-description",("itemName", Loc.GetString(PrototypeName)));
-
-        public SpriteSpecifier Icon => new SpriteSpecifier.EntityPrototype(_prototypeId);
-
-        public float Progress
-        {
-            get
-            {
-                var entMan = IoCManager.Resolve<IEntityManager>();
-
-                // TODO make this a container system function
-                // or: just iterate through transform children, instead of containers?
-
-                var metaQuery = entMan.GetEntityQuery<MetaDataComponent>();
-                var managerQuery = entMan.GetEntityQuery<ContainerManagerComponent>();
-                var stack = new Stack<ContainerManagerComponent>();
-
-                if (!entMan.TryGetComponent(_mind, out MindComponent? mind))
-                    return 0;
-
-                if (!metaQuery.TryGetComponent(mind.OwnedEntity, out var meta))
-                    return 0;
-
-                if (meta.EntityPrototype?.ID == _prototypeId)
-                    return 1;
-
-                if (!managerQuery.TryGetComponent(mind.OwnedEntity, out var currentManager))
-                    return 0;
-
-                do
-                {
-                    foreach (var container in currentManager.Containers.Values)
-                    {
-                        foreach (var entity in container.ContainedEntities)
-                        {
-                            if (metaQuery.GetComponent(entity).EntityPrototype?.ID == _prototypeId)
-                                return 1;
-                            if (!managerQuery.TryGetComponent(entity, out var containerManager))
-                                continue;
-                            stack.Push(containerManager);
-                        }
-                    }
-                } while (stack.TryPop(out currentManager));
-
-                return 0;
-            }
-        }
-
-        public float Difficulty => 2.25f;
-
-        public bool Equals(IObjectiveCondition? other)
-        {
-            return other is StealCondition stealCondition &&
-                   Equals(_mind, stealCondition._mind) &&
-                   _prototypeId == stealCondition._prototypeId;
-        }
-
-        public override bool Equals(object? obj)
-        {
-            if (ReferenceEquals(null, obj)) return false;
-            if (ReferenceEquals(this, obj)) return true;
-            if (obj.GetType() != GetType()) return false;
-            return Equals((StealCondition) obj);
-        }
-
-        public override int GetHashCode()
-        {
-            return HashCode.Combine(_mind, _prototypeId);
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/StealResearchCondition.cs b/Content.Server/Objectives/Conditions/StealResearchCondition.cs
deleted file mode 100644 (file)
index 4c32f3c..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-using Content.Server.Roles;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using Robust.Shared.Random;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions;
-
-/// <summary>
-/// Objective condition that requires the player to be a ninja and have stolen at least a random number of technologies.
-/// </summary>
-[DataDefinition]
-public sealed partial class StealResearchCondition : IObjectiveCondition
-{
-    private EntityUid? _mind;
-    private int _target;
-
-    public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
-    {
-        // TODO: clamp to number of research nodes in a single discipline maybe so easily maintainable
-        return new StealResearchCondition {
-            _mind = uid,
-            _target = IoCManager.Resolve<IRobustRandom>().Next(5, 10)
-        };
-    }
-
-    public string Title => Loc.GetString("objective-condition-steal-research-title", ("count", _target));
-
-    public string Description => Loc.GetString("objective-condition-steal-research-description");
-
-    public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Structures/Machines/server.rsi"), "server");
-
-    public float Progress
-    {
-        get
-        {
-            // prevent divide-by-zero
-            if (_target == 0)
-                return 1f;
-
-            var entMan = IoCManager.Resolve<IEntityManager>();
-            if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
-                return 0f;
-
-            if (role.DownloadedNodes.Count >= _target)
-                return 1f;
-
-            return (float) role.DownloadedNodes.Count / (float) _target;
-        }
-    }
-
-    public float Difficulty => 2.5f;
-
-    public bool Equals(IObjectiveCondition? other)
-    {
-        return other is StealResearchCondition cond && Equals(_mind, cond._mind) && _target == cond._target;
-    }
-
-    public override bool Equals(object? obj)
-    {
-        if (ReferenceEquals(null, obj)) return false;
-        if (ReferenceEquals(this, obj)) return true;
-        return obj is StealResearchCondition cond && cond.Equals(this);
-    }
-
-    public override int GetHashCode()
-    {
-        return HashCode.Combine(_mind?.GetHashCode() ?? 0, _target);
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/SurviveCondition.cs b/Content.Server/Objectives/Conditions/SurviveCondition.cs
deleted file mode 100644 (file)
index 98b5aa6..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions;
-
-/// <summary>
-/// Just requires that the player is not dead, ignores evac and what not.
-/// </summary>
-[DataDefinition]
-public sealed partial class SurviveCondition : IObjectiveCondition
-{
-    private EntityUid? _mind;
-
-    public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
-    {
-        return new SurviveCondition {_mind = uid};
-    }
-
-    public string Title => Loc.GetString("objective-condition-survive-title");
-
-    public string Description => Loc.GetString("objective-condition-survive-description");
-
-    public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Clothing/Mask/ninja.rsi"), "icon");
-
-    public float Difficulty => 0.5f;
-
-    public float Progress
-    {
-        get
-        {
-            var entMan = IoCManager.Resolve<IEntityManager>();
-            if (!entMan.TryGetComponent<MindComponent>(_mind, out var mind))
-                return 0f;
-
-            var mindSystem = entMan.System<SharedMindSystem>();
-            return mindSystem.IsCharacterDeadIc(mind) ? 0f : 1f;
-        }
-    }
-
-    public bool Equals(IObjectiveCondition? other)
-    {
-        return other is SurviveCondition condition && Equals(_mind, condition._mind);
-    }
-
-    public override bool Equals(object? obj)
-    {
-        if (ReferenceEquals(null, obj)) return false;
-        if (ReferenceEquals(this, obj)) return true;
-        if (obj.GetType() != GetType()) return false;
-        return Equals((SurviveCondition) obj);
-    }
-
-    public override int GetHashCode()
-    {
-        return (_mind != null ? _mind.GetHashCode() : 0);
-    }
-}
diff --git a/Content.Server/Objectives/Conditions/TerrorCondition.cs b/Content.Server/Objectives/Conditions/TerrorCondition.cs
deleted file mode 100644 (file)
index 28cce20..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-using Content.Server.Roles;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Objectives.Conditions;
-
-/// <summary>
-/// Objective condition that requires the player to be a ninja and have called in a threat.
-/// </summary>
-[DataDefinition]
-public sealed partial class TerrorCondition : IObjectiveCondition
-{
-    private EntityUid? _mind;
-
-    public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
-    {
-        return new TerrorCondition {_mind = uid};
-    }
-
-    public string Title => Loc.GetString("objective-condition-terror-title");
-
-    public string Description => Loc.GetString("objective-condition-terror-description");
-
-    public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Fun/Instruments/otherinstruments.rsi"), "red_phone");
-
-    public float Progress
-    {
-        get
-        {
-            var entMan = IoCManager.Resolve<EntityManager>();
-            if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
-                return 0f;
-
-            return role.CalledInThreat ? 1f : 0f;
-        }
-    }
-
-    public float Difficulty => 2.75f;
-
-    public bool Equals(IObjectiveCondition? other)
-    {
-        return other is TerrorCondition cond && Equals(_mind, cond._mind);
-    }
-
-    public override bool Equals(object? obj)
-    {
-        if (ReferenceEquals(null, obj)) return false;
-        if (ReferenceEquals(this, obj)) return true;
-        return obj is TerrorCondition cond && cond.Equals(this);
-    }
-
-    public override int GetHashCode()
-    {
-        return _mind?.GetHashCode() ?? 0;
-    }
-}
index 86cc58438a4511bd426870ee6ab2a432b80910b1..3f7e92963a83be0f5e6da933b8945fd8dfc65ee5 100644 (file)
@@ -2,7 +2,8 @@
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Mind;
 using Content.Shared.Mind;
-using Content.Shared.Objectives;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Objectives.Systems;
 using Content.Shared.Random;
 using Content.Shared.Random.Helpers;
 using Robust.Shared.Prototypes;
@@ -11,7 +12,7 @@ using System.Linq;
 
 namespace Content.Server.Objectives;
 
-public sealed class ObjectivesSystem : EntitySystem
+public sealed class ObjectivesSystem : SharedObjectivesSystem
 {
     [Dependency] private readonly GameTicker _gameTicker = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -30,6 +31,8 @@ public sealed class ObjectivesSystem : EntitySystem
     /// </summary>
     private void OnRoundEndText(RoundEndTextAppendEvent ev)
     {
+        // go through each gamerule getting data for the roundend summary.
+        var summaries = new Dictionary<string, Dictionary<string, List<EntityUid>>>();
         var query = EntityQueryEnumerator<GameRuleComponent>();
         while (query.MoveNext(out var uid, out var gameRule))
         {
@@ -41,90 +44,133 @@ public sealed class ObjectivesSystem : EntitySystem
             if (info.Minds.Count == 0)
                 continue;
 
+            // first group the gamerules by their agents, for example 2 different dragons
             var agent = info.AgentName;
-            var result = Loc.GetString("objectives-round-end-result", ("count", info.Minds.Count), ("agent", agent));
-            var prepend = new ObjectivesTextPrependEvent(result);
+            if (!summaries.ContainsKey(agent))
+                summaries[agent] = new Dictionary<string, List<EntityUid>>();
+
+            var prepend = new ObjectivesTextPrependEvent("");
             RaiseLocalEvent(uid, ref prepend);
-            // space between the start text and player list
-            result = prepend.Text + "\n";
 
-            foreach (var mindId in info.Minds)
+            // next group them by their prepended texts
+            // for example with traitor rule, group them by the codewords they share
+            var summary = summaries[agent];
+            if (summary.ContainsKey(prepend.Text))
             {
-                if (!TryComp(mindId, out MindComponent? mind))
-                    continue;
-
-                var name = mind.CharacterName;
-                _mind.TryGetSession(mindId, out var session);
-                var username = session?.Name;
+                // same prepended text (usually empty) so combine them
+                summary[prepend.Text].AddRange(info.Minds);
+            }
+            else
+            {
+                summary[prepend.Text] = info.Minds;
+            }
+        }
 
-                string title;
-                if (username != null)
-                {
-                    if (name != null)
-                        title = Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
-                    else
-                        title = Loc.GetString("objectives-player-user", ("user", username));
-                }
-                else
-                {
-                    // nothing to identify the player by, just give up
-                    if (name == null)
-                        continue;
+        // convert the data into summary text
+        foreach (var (agent, summary) in summaries)
+        {
+            // first get the total number of players that were in these game rules combined
+            var total = 0;
+            foreach (var (_, minds) in summary)
+            {
+                total += minds.Count;
+            }
 
-                    title = Loc.GetString("objectives-player-named", ("name", name));
-                }
+            var result = Loc.GetString("objectives-round-end-result", ("count", total), ("agent", agent));
+            // next add all the players with its own prepended text
+            foreach (var (prepend, minds) in summary)
+            {
+                if (prepend != string.Empty)
+                    result += prepend;
 
+                // add space between the start text and player list
                 result += "\n";
 
-                var objectives = mind.AllObjectives.ToArray();
-                if (objectives.Length == 0)
-                {
-                    result += Loc.GetString("objectives-no-objectives", ("title", title), ("agent", agent));
+                AddSummary(ref result, agent, minds);
+            }
+
+            ev.AddLine(result + "\n");
+        }
+    }
+
+    private void AddSummary(ref string result, string agent, List<EntityUid> minds)
+    {
+        foreach (var mindId in minds)
+        {
+            if (!TryComp(mindId, out MindComponent? mind))
+                continue;
+
+            var name = mind.CharacterName;
+            _mind.TryGetSession(mindId, out var session);
+            var username = session?.Name;
+
+            string title;
+            if (username != null)
+            {
+                if (name != null)
+                    title = Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
+                else
+                    title = Loc.GetString("objectives-player-user", ("user", username));
+            }
+            else
+            {
+                // nothing to identify the player by, just give up
+                if (name == null)
                     continue;
-                }
 
-                result += Loc.GetString("objectives-with-objectives", ("title", title), ("agent", agent));
+                title = Loc.GetString("objectives-player-named", ("name", name));
+            }
 
-                foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer))
+            result += "\n";
+
+            var objectives = mind.AllObjectives.ToArray();
+            if (objectives.Length == 0)
+            {
+                result += Loc.GetString("objectives-no-objectives", ("title", title), ("agent", agent));
+                continue;
+            }
+
+            result += Loc.GetString("objectives-with-objectives", ("title", title), ("agent", agent));
+
+            foreach (var objectiveGroup in objectives.GroupBy(o => Comp<ObjectiveComponent>(o).Issuer))
+            {
+                result += "\n" + Loc.GetString($"objective-issuer-{objectiveGroup.Key}");
+
+                foreach (var objective in objectiveGroup)
                 {
-                    result += "\n" + Loc.GetString($"objective-issuer-{objectiveGroup.Key}");
+                    var info = GetInfo(objective, mindId, mind);
+                    if (info == null)
+                        continue;
 
-                    foreach (var objective in objectiveGroup)
+                    var objectiveTitle = info.Value.Title;
+                    var progress = info.Value.Progress;
+                    if (progress > 0.99f)
                     {
-                        foreach (var condition in objective.Conditions)
-                        {
-                            var progress = condition.Progress;
-                            if (progress > 0.99f)
-                            {
-                                result += "\n- " + Loc.GetString(
-                                    "objectives-condition-success",
-                                    ("condition", condition.Title),
-                                    ("markupColor", "green")
-                                );
-                            }
-                            else
-                            {
-                                result += "\n- " + Loc.GetString(
-                                    "objectives-condition-fail",
-                                    ("condition", condition.Title),
-                                    ("progress", (int) (progress * 100)),
-                                    ("markupColor", "red")
-                                );
-                            }
-                        }
+                        result += "\n- " + Loc.GetString(
+                            "objectives-objective-success",
+                            ("objective", objectiveTitle),
+                            ("markupColor", "green")
+                        );
+                    }
+                    else
+                    {
+                        result += "\n- " + Loc.GetString(
+                            "objectives-objective-fail",
+                            ("objective", objectiveTitle),
+                            ("progress", (int) (progress * 100)),
+                            ("markupColor", "red")
+                        );
                     }
                 }
             }
-
-            ev.AddLine(result + "\n");
         }
     }
 
-    public ObjectivePrototype? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto)
+    public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto)
     {
         if (!_prototypeManager.TryIndex<WeightedRandomPrototype>(objectiveGroupProto, out var groups))
         {
-            Log.Error("Tried to get a random objective, but can't index WeightedRandomPrototype " + objectiveGroupProto);
+            Log.Error($"Tried to get a random objective, but can't index WeightedRandomPrototype {objectiveGroupProto}");
             return null;
         }
 
@@ -137,15 +183,16 @@ public sealed class ObjectivesSystem : EntitySystem
 
             if (!_prototypeManager.TryIndex<WeightedRandomPrototype>(groupName, out var group))
             {
-                Log.Error("Couldn't index objective group prototype" + groupName);
+                Log.Error($"Couldn't index objective group prototype {groupName}");
                 return null;
             }
 
-            if (_prototypeManager.TryIndex<ObjectivePrototype>(group.Pick(_random), out var objective)
-                && objective.CanBeAssigned(mindId, mind))
+            var proto = group.Pick(_random);
+            var objective = TryCreateObjective(mindId, mind, proto);
+            if (objective != null)
                 return objective;
-            else
-                tries++;
+
+            tries++;
         }
 
         return null;
diff --git a/Content.Server/Objectives/Requirements/IncompatibleConditionsRequirement.cs b/Content.Server/Objectives/Requirements/IncompatibleConditionsRequirement.cs
deleted file mode 100644 (file)
index 9a235e5..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-
-namespace Content.Server.Objectives.Requirements
-{
-    [DataDefinition]
-    public sealed partial class IncompatibleConditionsRequirement : IObjectiveRequirement
-    {
-        [DataField("conditions")]
-        private List<string> _incompatibleConditions = new();
-
-        public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
-        {
-            foreach (var objective in mind.AllObjectives)
-            {
-                foreach (var condition in objective.Conditions)
-                {
-                    foreach (var incompatibleCondition in _incompatibleConditions)
-                    {
-                        if (incompatibleCondition == condition.GetType().Name) return false;
-                    }
-                }
-            }
-
-            return true;
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Requirements/IncompatibleObjectivesRequirement.cs b/Content.Server/Objectives/Requirements/IncompatibleObjectivesRequirement.cs
deleted file mode 100644 (file)
index 57209dc..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-
-namespace Content.Server.Objectives.Requirements
-{
-    [DataDefinition]
-    public sealed partial class IncompatibleObjectivesRequirement : IObjectiveRequirement
-    {
-        [DataField("objectives")]
-        private List<string> _incompatibleObjectives = new();
-
-        public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
-        {
-            foreach (var objective in mind.AllObjectives)
-            {
-                foreach (var incompatibleObjective in _incompatibleObjectives)
-                {
-                    if (incompatibleObjective == objective.Prototype.ID) return false;
-                }
-            }
-
-            return true;
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs b/Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs
deleted file mode 100644 (file)
index 8268941..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-using Content.Server.GameTicking.Rules;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-
-namespace Content.Server.Objectives.Requirements
-{
-    [DataDefinition]
-    public sealed partial class MultipleTraitorsRequirement : IObjectiveRequirement
-    {
-        [DataField("traitors")]
-        private int _requiredTraitors = 2;
-
-        public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
-        {
-            return EntitySystem.Get<TraitorRuleSystem>().GetOtherTraitorMindsAliveAndConnected(mind).Count >= _requiredTraitors;
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Requirements/NinjaRequirement.cs b/Content.Server/Objectives/Requirements/NinjaRequirement.cs
deleted file mode 100644 (file)
index 8a0993b..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-using Content.Server.Roles;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-
-namespace Content.Server.Objectives.Requirements;
-
-/// <summary>
-/// Requires the player's mind to have the ninja role component, aka be a ninja.
-/// </summary>
-[DataDefinition]
-public sealed partial class NinjaRequirement : IObjectiveRequirement
-{
-    public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
-    {
-        var entMan = IoCManager.Resolve<IEntityManager>();
-        return entMan.HasComponent<NinjaRoleComponent>(mindId);
-    }
-}
diff --git a/Content.Server/Objectives/Requirements/NotRoleRequirement.cs b/Content.Server/Objectives/Requirements/NotRoleRequirement.cs
deleted file mode 100644 (file)
index f0b06f3..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using Content.Shared.Roles;
-using Content.Shared.Roles.Jobs;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Objectives.Requirements
-{
-    [DataDefinition]
-    public sealed partial class NotRoleRequirement : IObjectiveRequirement
-    {
-        [DataField("roleId", customTypeSerializer:typeof(PrototypeIdSerializer<JobPrototype>), required:true)]
-        private string _roleId = default!;
-
-        /// <summary>
-        /// This requirement is met if the traitor is NOT the roleId, and fails if they are.
-        /// </summary>
-        public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
-        {
-            // TODO ECS this shit i keep seeing shitcode everywhere
-            var entities = IoCManager.Resolve<IEntityManager>();
-            if (!entities.TryGetComponent(mindId, out JobComponent? job))
-                return true;
-
-            return job.PrototypeId != _roleId;
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Requirements/SpiderChargeTargetRequirement.cs b/Content.Server/Objectives/Requirements/SpiderChargeTargetRequirement.cs
deleted file mode 100644 (file)
index 6bb6bbb..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using Content.Server.Roles;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-
-namespace Content.Server.Objectives.Requirements;
-
-/// <summary>
-/// Requires the player to be a ninja that has a spider charge target assigned, which is almost always the case.
-/// </summary>
-[DataDefinition]
-public sealed partial class SpiderChargeTargetRequirement : IObjectiveRequirement
-{
-    public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
-    {
-        var entMan = IoCManager.Resolve<IEntityManager>();
-        entMan.TryGetComponent<NinjaRoleComponent>(mindId, out var role);
-        return role?.SpiderChargeTarget != null;
-    }
-}
diff --git a/Content.Server/Objectives/Requirements/TraitorRequirement.cs b/Content.Server/Objectives/Requirements/TraitorRequirement.cs
deleted file mode 100644 (file)
index e2bab8f..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using Content.Server.Roles;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using Content.Shared.Roles;
-using JetBrains.Annotations;
-
-namespace Content.Server.Objectives.Requirements
-{
-    [UsedImplicitly]
-    [DataDefinition]
-    public sealed partial class TraitorRequirement : IObjectiveRequirement
-    {
-        public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
-        {
-            var roleSystem = IoCManager.Resolve<IEntityManager>().System<SharedRoleSystem>();
-            return roleSystem.MindHasRole<TraitorRoleComponent>(mindId);
-        }
-    }
-}
diff --git a/Content.Server/Objectives/Systems/DieConditionSystem.cs b/Content.Server/Objectives/Systems/DieConditionSystem.cs
new file mode 100644 (file)
index 0000000..8bc3846
--- /dev/null
@@ -0,0 +1,22 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+
+namespace Content.Server.Objectives.Systems;
+
+public sealed class DieConditionSystem : EntitySystem
+{
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<DieConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
+    }
+
+    private void OnGetProgress(EntityUid uid, DieConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        args.Progress = _mind.IsCharacterDeadIc(args.Mind) ? 1f : 0f;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/EscapeShuttleConditionSystem.cs b/Content.Server/Objectives/Systems/EscapeShuttleConditionSystem.cs
new file mode 100644 (file)
index 0000000..5f8f680
--- /dev/null
@@ -0,0 +1,39 @@
+using Content.Server.Objectives.Components;
+using Content.Server.Shuttles.Systems;
+using Content.Shared.Cuffs.Components;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+
+namespace Content.Server.Objectives.Systems;
+
+public sealed class EscapeShuttleConditionSystem : EntitySystem
+{
+    [Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<EscapeShuttleConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
+    }
+
+    private void OnGetProgress(EntityUid uid, EscapeShuttleConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        args.Progress = GetProgress(args.MindId, args.Mind);
+    }
+
+    private float GetProgress(EntityUid mindId, MindComponent mind)
+    {
+        // not escaping alive if you're deleted/dead
+        if (mind.OwnedEntity == null || _mind.IsCharacterDeadIc(mind))
+            return 0f;
+
+        // You're not escaping if you're restrained!
+        if (TryComp<CuffableComponent>(mind.OwnedEntity, out var cuffed) && cuffed.CuffedHandCount > 0)
+            return 0f;
+
+        // Any emergency shuttle counts for this objective, but not pods.
+        return _emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value) ? 1f : 0f;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/HelpProgressConditionSystem.cs b/Content.Server/Objectives/Systems/HelpProgressConditionSystem.cs
new file mode 100644 (file)
index 0000000..e4455c0
--- /dev/null
@@ -0,0 +1,111 @@
+using Content.Server.GameTicking.Rules;
+using Content.Server.Objectives.Components;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Objectives.Systems;
+using Content.Shared.Roles.Jobs;
+using Robust.Shared.Random;
+using System.Linq;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Handles help progress condition logic and picking random help targets.
+/// </summary>
+public sealed class HelpProgressConditionSystem : EntitySystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly SharedObjectivesSystem _objectives = default!;
+    [Dependency] private readonly TargetObjectiveSystem _target = default!;
+    [Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<HelpProgressConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
+
+        SubscribeLocalEvent<RandomTraitorProgressComponent, ObjectiveAssignedEvent>(OnTraitorAssigned);
+    }
+
+    private void OnGetProgress(EntityUid uid, HelpProgressConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        if (!_target.GetTarget(uid, out var target))
+            return;
+
+        args.Progress = GetProgress(target.Value);
+    }
+
+    private void OnTraitorAssigned(EntityUid uid, RandomTraitorProgressComponent comp, ref ObjectiveAssignedEvent args)
+    {
+        // invalid prototype
+        if (!TryComp<TargetObjectiveComponent>(uid, out var target))
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind)
+            .Select(pair => pair.Item1)
+            .ToHashSet();
+        var removeList = new List<EntityUid>();
+
+        // cant help anyone who is tasked with helping:
+        // 1. thats boring
+        // 2. no cyclic progress dependencies!!!
+        foreach (var traitor in traitors)
+        {
+            // TODO: replace this with TryComp<ObjectivesComponent>(traitor) or something when objectives are moved out of mind
+            if (!TryComp<MindComponent>(traitor, out var mind))
+                continue;
+
+            foreach (var objective in mind.AllObjectives)
+            {
+                if (HasComp<HelpProgressConditionComponent>(objective))
+                    removeList.Add(traitor);
+            }
+        }
+
+        foreach (var tot in removeList)
+        {
+            traitors.Remove(tot);
+        }
+
+        // no more helpable traitors
+        if (traitors.Count == 0)
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        _target.SetTarget(uid, _random.Pick(traitors), target);
+    }
+
+    private float GetProgress(EntityUid target)
+    {
+        var total = 0f; // how much progress they have
+        var max = 0f; // how much progress is needed for 100%
+
+        if (TryComp<MindComponent>(target, out var mind))
+        {
+            foreach (var objective in mind.AllObjectives)
+            {
+                // this has the potential to loop forever, anything setting target has to check that there is no HelpProgressCondition.
+                var info = _objectives.GetInfo(objective, target, mind);
+                if (info == null)
+                    continue;
+
+                max++; // things can only be up to 100% complete yeah
+                total += info.Value.Progress;
+            }
+        }
+
+        // no objectives that can be helped with...
+        if (max == 0f)
+            return 1f;
+
+        // require 50% completion for this one to be complete
+        var completion = total / max;
+        return completion >= 0.5f ? 1f : completion / 0.5f;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/KeepAliveCondition.cs b/Content.Server/Objectives/Systems/KeepAliveCondition.cs
new file mode 100644 (file)
index 0000000..48df96e
--- /dev/null
@@ -0,0 +1,66 @@
+using Content.Server.Objectives.Components;
+using Content.Server.GameTicking.Rules;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Roles.Jobs;
+using Robust.Shared.Random;
+using System.Linq;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Handles keep alive condition logic and picking random traitors to keep alive.
+/// </summary>
+public sealed class KeepAliveConditionSystem : EntitySystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+    [Dependency] private readonly TargetObjectiveSystem _target = default!;
+    [Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<KeepAliveConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
+
+        SubscribeLocalEvent<RandomTraitorAliveComponent, ObjectiveAssignedEvent>(OnAssigned);
+    }
+
+    private void OnGetProgress(EntityUid uid, KeepAliveConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        if (!_target.GetTarget(uid, out var target))
+            return;
+
+        args.Progress = GetProgress(target.Value);
+    }
+
+    private void OnAssigned(EntityUid uid, RandomTraitorAliveComponent comp, ref ObjectiveAssignedEvent args)
+    {
+        // invalid prototype
+        if (!TryComp<TargetObjectiveComponent>(uid, out var target))
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        var traitors = Enumerable.ToList<(EntityUid Id, MindComponent Mind)>(_traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind));
+
+        // You are the first/only traitor.
+        if (traitors.Count == 0)
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        _target.SetTarget(uid, _random.Pick(traitors).Id, target);
+    }
+
+    private float GetProgress(EntityUid target)
+    {
+        if (!TryComp<MindComponent>(target, out var mind))
+            return 0f;
+
+        return _mind.IsCharacterDeadIc(mind) ? 0f : 1f;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/KillPersonConditionSystem.cs b/Content.Server/Objectives/Systems/KillPersonConditionSystem.cs
new file mode 100644 (file)
index 0000000..c1caa81
--- /dev/null
@@ -0,0 +1,131 @@
+using Content.Server.Objectives.Components;
+using Content.Server.Shuttles.Systems;
+using Content.Shared.CCVar;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Roles.Jobs;
+using Robust.Shared.Configuration;
+using Robust.Shared.Random;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Handles kill person condition logic and picking random kill targets.
+/// </summary>
+public sealed class KillPersonConditionSystem : EntitySystem
+{
+    [Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
+    [Dependency] private readonly IConfigurationManager _config = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly SharedJobSystem _job = default!;
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+    [Dependency] private readonly TargetObjectiveSystem _target = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<KillPersonConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
+
+        SubscribeLocalEvent<PickRandomPersonComponent, ObjectiveAssignedEvent>(OnPersonAssigned);
+
+        SubscribeLocalEvent<PickRandomHeadComponent, ObjectiveAssignedEvent>(OnHeadAssigned);
+    }
+
+    private void OnGetProgress(EntityUid uid, KillPersonConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        if (!_target.GetTarget(uid, out var target))
+            return;
+
+        args.Progress = GetProgress(target.Value, comp.RequireDead);
+    }
+
+    private void OnPersonAssigned(EntityUid uid, PickRandomPersonComponent comp, ref ObjectiveAssignedEvent args)
+    {
+        // invalid objective prototype
+        if (!TryComp<TargetObjectiveComponent>(uid, out var target))
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        // target already assigned
+        if (target.Target != null)
+            return;
+
+        // no other humans to kill
+        var allHumans = _mind.GetAliveHumansExcept(args.MindId);
+        if (allHumans.Count == 0)
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        _target.SetTarget(uid, _random.Pick(allHumans), target);
+    }
+
+    private void OnHeadAssigned(EntityUid uid, PickRandomHeadComponent comp, ref ObjectiveAssignedEvent args)
+    {
+        // invalid prototype
+        if (!TryComp<TargetObjectiveComponent>(uid, out var target))
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        // target already assigned
+        if (target.Target != null)
+            return;
+
+        // no other humans to kill
+        var allHumans = _mind.GetAliveHumansExcept(args.MindId);
+        if (allHumans.Count == 0)
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        var allHeads = new List<EntityUid>();
+        foreach (var mind in allHumans)
+        {
+            // RequireAdminNotify used as a cheap way to check for command department
+            if (_job.MindTryGetJob(mind, out _, out var prototype) && prototype.RequireAdminNotify)
+                allHeads.Add(mind);
+        }
+
+        if (allHeads.Count == 0)
+            allHeads = allHumans; // fallback to non-head target
+
+        _target.SetTarget(uid, _random.Pick(allHeads), target);
+    }
+
+    private float GetProgress(EntityUid target, bool requireDead)
+    {
+        // deleted or gibbed or something, counts as dead
+        if (!TryComp<MindComponent>(target, out var mind) || mind.OwnedEntity == null)
+            return 1f;
+
+        // dead is success
+        if (_mind.IsCharacterDeadIc(mind))
+            return 1f;
+
+        // if the target has to be dead dead then don't check evac stuff
+        if (requireDead)
+            return 0f;
+
+        // if evac is disabled then they really do have to be dead
+        if (!_config.GetCVar(CCVars.EmergencyShuttleEnabled))
+            return 0f;
+
+        // target is escaping so you fail
+        if (_emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value))
+            return 0f;
+
+        // evac has left without the target, greentext since the target is afk in space with a full oxygen tank and coordinates off.
+        if (_emergencyShuttle.ShuttlesLeft)
+            return 1f;
+
+        // if evac is still here and target hasn't boarded, show 50% to give you an indicator that you are doing good
+        return _emergencyShuttle.EmergencyShuttleArrived ? 0.5f : 0f;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/MultipleTraitorsRequirementSystem.cs b/Content.Server/Objectives/Systems/MultipleTraitorsRequirementSystem.cs
new file mode 100644 (file)
index 0000000..7581119
--- /dev/null
@@ -0,0 +1,29 @@
+using Content.Server.GameTicking.Rules;
+using Content.Server.Objectives.Components;
+using Content.Shared.Objectives.Components;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Handles requiring multiple traitors being alive for the objective to be given.
+/// </summary>
+public sealed class MultipleTraitorsRequirementSystem : EntitySystem
+{
+    [Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<MultipleTraitorsRequirementComponent, RequirementCheckEvent>(OnCheck);
+    }
+
+    private void OnCheck(EntityUid uid, MultipleTraitorsRequirementComponent comp, ref RequirementCheckEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        if (_traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).Count < comp.Traitors)
+            args.Cancelled = true;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/NinjaConditionsSystem.cs b/Content.Server/Objectives/Systems/NinjaConditionsSystem.cs
new file mode 100644 (file)
index 0000000..8e03ef2
--- /dev/null
@@ -0,0 +1,106 @@
+using Content.Server.Roles;
+using Content.Server.Objectives.Components;
+using Content.Server.Warps;
+using Content.Shared.Objectives.Components;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Handles the objective conditions that hard depend on ninja.
+/// Survive is handled by <see cref="SurviveConditionSystem"/> since it works without being a ninja.
+/// </summary>
+public sealed class NinjaConditionsSystem : EntitySystem
+{
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly NumberObjectiveSystem _number = default!;
+
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<DoorjackConditionComponent, ObjectiveGetProgressEvent>(OnDoorjackGetProgress);
+
+        SubscribeLocalEvent<SpiderChargeConditionComponent, ObjectiveAfterAssignEvent>(OnSpiderChargeAfterAssign);
+        SubscribeLocalEvent<SpiderChargeConditionComponent, ObjectiveGetProgressEvent>(OnSpiderChargeGetProgress);
+
+        SubscribeLocalEvent<StealResearchConditionComponent, ObjectiveGetProgressEvent>(OnStealResearchGetProgress);
+
+        SubscribeLocalEvent<TerrorConditionComponent, ObjectiveGetProgressEvent>(OnTerrorGetProgress);
+    }
+
+    // doorjack
+
+    private void OnDoorjackGetProgress(EntityUid uid, DoorjackConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        args.Progress = DoorjackProgress(args.MindId, _number.GetTarget(uid));
+    }
+
+    private float DoorjackProgress(EntityUid mindId, int target)
+    {
+        // prevent divide-by-zero
+        if (target == 0)
+            return 1f;
+
+        if (!TryComp<NinjaRoleComponent>(mindId, out var role))
+            return 0f;
+
+        if (role.DoorsJacked >= target)
+            return 1f;
+
+        return (float) role.DoorsJacked / (float) target;
+    }
+
+    // spider charge
+
+    private void OnSpiderChargeAfterAssign(EntityUid uid, SpiderChargeConditionComponent comp, ref ObjectiveAfterAssignEvent args)
+    {
+        _metaData.SetEntityName(uid, SpiderChargeTitle(args.MindId), args.Meta);
+    }
+
+    private void OnSpiderChargeGetProgress(EntityUid uid, SpiderChargeConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        args.Progress = TryComp<NinjaRoleComponent>(args.MindId, out var role) && role.SpiderChargeDetonated ? 1f : 0f;
+    }
+
+    private string SpiderChargeTitle(EntityUid mindId)
+    {
+        if (!TryComp<NinjaRoleComponent>(mindId, out var role) ||
+            role.SpiderChargeTarget == null ||
+            !TryComp<WarpPointComponent>(role.SpiderChargeTarget, out var warp) ||
+            warp.Location == null)
+        {
+            // this should never really happen but eh
+            return Loc.GetString("objective-condition-spider-charge-title-no-target");
+        }
+
+        return Loc.GetString("objective-condition-spider-charge-title", ("location", warp.Location));
+    }
+
+    // steal research
+
+    private void OnStealResearchGetProgress(EntityUid uid, StealResearchConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        args.Progress = StealResearchProgress(args.MindId, _number.GetTarget(uid));
+    }
+
+    private float StealResearchProgress(EntityUid mindId, int target)
+    {
+        // prevent divide-by-zero
+        if (target == 0)
+            return 1f;
+
+        if (!TryComp<NinjaRoleComponent>(mindId, out var role))
+            return 0f;
+
+        if (role.DownloadedNodes.Count >= target)
+            return 1f;
+
+        return (float) role.DownloadedNodes.Count / (float) target;
+    }
+
+    // terror
+
+    private void OnTerrorGetProgress(EntityUid uid, TerrorConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        args.Progress = TryComp<NinjaRoleComponent>(args.MindId, out var role) && role.CalledInThreat ? 1f : 0f;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/NotCommandRequirementSystem.cs b/Content.Server/Objectives/Systems/NotCommandRequirementSystem.cs
new file mode 100644 (file)
index 0000000..e63492b
--- /dev/null
@@ -0,0 +1,27 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Roles.Jobs;
+
+namespace Content.Server.Objectives.Systems;
+
+public sealed class NotCommandRequirementSystem : EntitySystem
+{
+    [Dependency] private readonly SharedJobSystem _job = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<NotCommandRequirementComponent, RequirementCheckEvent>(OnCheck);
+    }
+
+    private void OnCheck(EntityUid uid, NotCommandRequirementComponent comp, ref RequirementCheckEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        // cheap equivalent to checking that job department is command, since all command members require admin notification when leaving
+        if (_job.MindTryGetJob(args.MindId, out _, out var prototype) && prototype.RequireAdminNotify)
+            args.Cancelled = true;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs b/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs
new file mode 100644 (file)
index 0000000..c9539fc
--- /dev/null
@@ -0,0 +1,31 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Roles.Jobs;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Handles checking the job blacklist for this objective.
+/// </summary>
+public sealed class NotJobRequirementSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<NotJobRequirementComponent, RequirementCheckEvent>(OnCheck);
+    }
+
+    private void OnCheck(EntityUid uid, NotJobRequirementComponent comp, ref RequirementCheckEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        // if player has no job then don't care
+        if (!TryComp<JobComponent>(args.MindId, out var job))
+            return;
+
+        if (job.PrototypeId == comp.Job)
+            args.Cancelled = true;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/NumberObjectiveSystem.cs b/Content.Server/Objectives/Systems/NumberObjectiveSystem.cs
new file mode 100644 (file)
index 0000000..3263284
--- /dev/null
@@ -0,0 +1,48 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Objectives.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Random;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Provides API for other components, handles picking the count and setting the title and description.
+/// </summary>
+public sealed class NumberObjectiveSystem : EntitySystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<NumberObjectiveComponent, ObjectiveAssignedEvent>(OnAssigned);
+        SubscribeLocalEvent<NumberObjectiveComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
+    }
+
+    private void OnAssigned(EntityUid uid, NumberObjectiveComponent comp, ref ObjectiveAssignedEvent args)
+    {
+        comp.Target = _random.Next(comp.Min, comp.Max);
+    }
+
+    private void OnAfterAssign(EntityUid uid, NumberObjectiveComponent comp, ref ObjectiveAfterAssignEvent args)
+    {
+        if (comp.Title != null)
+            _metaData.SetEntityName(uid, Loc.GetString(comp.Title, ("count", comp.Target)), args.Meta);
+
+        if (comp.Description != null)
+            _metaData.SetEntityDescription(uid, Loc.GetString(comp.Description, ("count", comp.Target)), args.Meta);
+    }
+
+    /// <summary>
+    /// Gets the objective's target count.
+    /// </summary>
+    public int GetTarget(EntityUid uid, NumberObjectiveComponent? comp = null)
+    {
+        if (!Resolve(uid, ref comp))
+            return 0;
+
+        return comp.Target;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs b/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs
new file mode 100644 (file)
index 0000000..5318b2e
--- /dev/null
@@ -0,0 +1,26 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Objectives.Components;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Handles applying the objective component blacklist to the objective entity.
+/// </summary>
+public sealed class ObjectiveBlacklistRequirementSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<ObjectiveBlacklistRequirementComponent, RequirementCheckEvent>(OnCheck);
+    }
+
+    private void OnCheck(EntityUid uid, ObjectiveBlacklistRequirementComponent comp, ref RequirementCheckEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        if (comp.Blacklist.IsValid(uid, EntityManager))
+            args.Cancelled = true;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/RoleRequirementSystem.cs b/Content.Server/Objectives/Systems/RoleRequirementSystem.cs
new file mode 100644 (file)
index 0000000..97aee21
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Objectives.Components;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Handles role requirement for objectives that require a certain (probably antagonist) role(s).
+/// </summary>
+public sealed class RoleRequirementSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RoleRequirementComponent, RequirementCheckEvent>(OnCheck);
+    }
+
+    private void OnCheck(EntityUid uid, RoleRequirementComponent comp, ref RequirementCheckEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        // this whitelist trick only works because roles are components on the mind and not entities
+        // if that gets reworked then this will need changing
+        if (!comp.Roles.IsValid(args.MindId, EntityManager))
+            args.Cancelled = true;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/SpiderChargeTargetRequirementSystem.cs b/Content.Server/Objectives/Systems/SpiderChargeTargetRequirementSystem.cs
new file mode 100644 (file)
index 0000000..107d889
--- /dev/null
@@ -0,0 +1,24 @@
+using Content.Server.Objectives.Components;
+using Content.Server.Roles;
+using Content.Shared.Objectives.Components;
+
+namespace Content.Server.Objectives.Systems;
+
+public sealed class SpiderChargeTargetRequirementSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<SpiderChargeTargetRequirementComponent, RequirementCheckEvent>(OnCheck);
+    }
+
+    private void OnCheck(EntityUid uid, SpiderChargeTargetRequirementComponent comp, ref RequirementCheckEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        if (!TryComp<NinjaRoleComponent>(args.MindId, out var role) || role.SpiderChargeTarget == null)
+            args.Cancelled = true;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/StealConditionSystem.cs b/Content.Server/Objectives/Systems/StealConditionSystem.cs
new file mode 100644 (file)
index 0000000..28ab164
--- /dev/null
@@ -0,0 +1,93 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Objectives.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Objectives.Systems;
+
+public sealed class StealConditionSystem : EntitySystem
+{
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly SharedObjectivesSystem _objectives = default!;
+
+    private EntityQuery<ContainerManagerComponent> containerQuery;
+    private EntityQuery<MetaDataComponent> metaQuery;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        containerQuery = GetEntityQuery<ContainerManagerComponent>();
+        metaQuery = GetEntityQuery<MetaDataComponent>();
+
+        SubscribeLocalEvent<StealConditionComponent, ObjectiveAssignedEvent>(OnAssigned);
+        SubscribeLocalEvent<StealConditionComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
+        SubscribeLocalEvent<StealConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
+    }
+
+    private void OnAssigned(EntityUid uid, StealConditionComponent comp, ref ObjectiveAssignedEvent args)
+    {
+        // cancel if the item to steal doesn't exist
+        args.Cancelled |= !_proto.HasIndex<EntityPrototype>(comp.Prototype);
+    }
+
+    private void OnAfterAssign(EntityUid uid, StealConditionComponent comp, ref ObjectiveAfterAssignEvent args)
+    {
+        var proto = _proto.Index<EntityPrototype>(comp.Prototype);
+        var title = comp.OwnerText == null
+            ? Loc.GetString("objective-condition-steal-title-no-owner", ("itemName", proto.Name))
+            : Loc.GetString("objective-condition-steal-title", ("owner", Loc.GetString(comp.OwnerText)), ("itemName", proto.Name));
+        var description = Loc.GetString("objective-condition-steal-description", ("itemName", proto.Name));
+
+        _metaData.SetEntityName(uid, title, args.Meta);
+        _metaData.SetEntityDescription(uid, description, args.Meta);
+        _objectives.SetIcon(uid, new SpriteSpecifier.EntityPrototype(comp.Prototype), args.Objective);
+    }
+
+    private void OnGetProgress(EntityUid uid, StealConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        args.Progress = GetProgress(args.Mind, comp.Prototype);
+    }
+
+    private float GetProgress(MindComponent mind, string prototype)
+    {
+        // TODO make this a container system function
+        // or: just iterate through transform children, instead of containers?
+
+        if (!metaQuery.TryGetComponent(mind.OwnedEntity, out var meta))
+            return 0;
+
+        // who added this check bruh
+        if (meta.EntityPrototype?.ID == prototype)
+            return 1;
+
+        if (!containerQuery.TryGetComponent(mind.OwnedEntity, out var currentManager))
+            return 0;
+
+        // recursively check each container for the item
+        // checks inventory, bag, implants, etc.
+        var stack = new Stack<ContainerManagerComponent>();
+        do
+        {
+            foreach (var container in currentManager.Containers.Values)
+            {
+                foreach (var entity in container.ContainedEntities)
+                {
+                    // check if this is the item
+                    if (metaQuery.GetComponent(entity).EntityPrototype?.ID == prototype)
+                        return 1;
+
+                    // if it is a container check its contents
+                    if (containerQuery.TryGetComponent(entity, out var containerManager))
+                        stack.Push(containerManager);
+                }
+            }
+        } while (stack.TryPop(out currentManager));
+
+        return 0;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/SurviveConditionSystem.cs b/Content.Server/Objectives/Systems/SurviveConditionSystem.cs
new file mode 100644 (file)
index 0000000..9bb7234
--- /dev/null
@@ -0,0 +1,25 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Mind;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Handles progress for the survive objective condition.
+/// </summary>
+public sealed class SurviveConditionSystem : EntitySystem
+{
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<SurviveConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
+    }
+
+    private void OnGetProgress(EntityUid uid, SurviveConditionComponent comp, ref ObjectiveGetProgressEvent args)
+    {
+        args.Progress = _mind.IsCharacterDeadIc(args.Mind) ? 0f : 1f;
+    }
+}
diff --git a/Content.Server/Objectives/Systems/TargetObjectiveSystem.cs b/Content.Server/Objectives/Systems/TargetObjectiveSystem.cs
new file mode 100644 (file)
index 0000000..82ebd28
--- /dev/null
@@ -0,0 +1,68 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Roles.Jobs;
+using Robust.Shared.GameObjects;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Content.Server.Objectives.Systems;
+
+/// <summary>
+/// Provides API for other components and handles setting the title.
+/// </summary>
+public sealed class TargetObjectiveSystem : EntitySystem
+{
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly SharedJobSystem _job = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<TargetObjectiveComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
+    }
+
+    private void OnAfterAssign(EntityUid uid, TargetObjectiveComponent comp, ref ObjectiveAfterAssignEvent args)
+    {
+        if (!GetTarget(uid, out var target, comp))
+            return;
+
+        _metaData.SetEntityName(uid, GetTitle(target.Value, comp.Title), args.Meta);
+    }
+
+    /// <summary>
+    /// Sets the Target field for the title and other components to use.
+    /// </summary>
+    public void SetTarget(EntityUid uid, EntityUid target, TargetObjectiveComponent? comp = null)
+    {
+        if (!Resolve(uid, ref comp))
+            return;
+
+        comp.Target = target;
+    }
+
+    /// <summary>
+    /// Gets the target from the component.
+    /// </summary>
+    /// <remarks>
+    /// If it is null then the prototype is invalid, just return.
+    /// </remarks>
+    public bool GetTarget(EntityUid uid, [NotNullWhen(true)] out EntityUid? target, TargetObjectiveComponent? comp = null)
+    {
+        target = Resolve(uid, ref comp) ? comp.Target : null;
+        return target != null;
+    }
+
+    private string GetTitle(EntityUid target, string title)
+    {
+        var targetName = "Unknown";
+        if (TryComp<MindComponent>(target, out var mind) && mind.CharacterName != null)
+        {
+            targetName = mind.CharacterName;
+        }
+
+        var jobName = _job.MindTryGetJobName(target);
+        return Loc.GetString(title, ("targetName", targetName), ("job", jobName));
+    }
+
+}
index b330af2629936e57b54771acd9e1d0ac2d2122f5..550a81313a9d04c2aefac4c0d4e8f39f275d4c9e 100644 (file)
@@ -19,10 +19,10 @@ public sealed class CharacterInfoEvent : EntityEventArgs
 {
     public readonly NetEntity NetEntity;
     public readonly string JobTitle;
-    public readonly Dictionary<string, List<ConditionInfo>> Objectives;
+    public readonly Dictionary<string, List<ObjectiveInfo>> Objectives;
     public readonly string? Briefing;
 
-    public CharacterInfoEvent(NetEntity netEntity, string jobTitle, Dictionary<string, List<ConditionInfo>> objectives, string? briefing)
+    public CharacterInfoEvent(NetEntity netEntity, string jobTitle, Dictionary<string, List<ObjectiveInfo>> objectives, string? briefing)
     {
         NetEntity = netEntity;
         JobTitle = jobTitle;
index f195220e14b79302d2ddc829b8ed818c63fb583e..d6e30130e7d1f654d975779225eb9be10e0c13fb 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Shared.GameTicking;
 using Content.Shared.Mind.Components;
-using Content.Shared.Objectives;
 using Robust.Shared.Network;
 using Robust.Shared.Players;
 
@@ -22,7 +21,7 @@ namespace Content.Shared.Mind
     [RegisterComponent]
     public sealed partial class MindComponent : Component
     {
-        internal readonly List<Objective> Objectives = new();
+        internal readonly List<EntityUid> Objectives = new();
 
         /// <summary>
         ///     The session ID of the player owning this mind.
@@ -78,10 +77,10 @@ namespace Content.Shared.Mind
 
         // TODO move objectives out of mind component
         /// <summary>
-        ///     An enumerable over all the objectives this mind has.
+        ///     An enumerable over all the objective entities this mind has.
         /// </summary>
         [ViewVariables]
-        public IEnumerable<Objective> AllObjectives => Objectives;
+        public IEnumerable<EntityUid> AllObjectives => Objectives;
 
         /// <summary>
         ///     Prevents user from ghosting out
index fc6cb8d570baa67fd4fe78f6aba5630b5c9fa116..91f68b024543a2398861581eecdc7e7e1ee7e525 100644 (file)
@@ -3,11 +3,13 @@ using Content.Shared.Administration.Logs;
 using Content.Shared.Database;
 using Content.Shared.Examine;
 using Content.Shared.GameTicking;
+using Content.Shared.Humanoid;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Mind.Components;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Objectives;
+using Content.Shared.Objectives.Systems;
 using Content.Shared.Players;
 using Robust.Shared.Map;
 using Robust.Shared.Network;
@@ -20,9 +22,9 @@ namespace Content.Shared.Mind;
 public abstract class SharedMindSystem : EntitySystem
 {
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly IPrototypeManager _proto = default!;
-    [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
-    [Dependency] private readonly SharedPlayerSystem _playerSystem = default!;
+    [Dependency] private readonly MobStateSystem _mobState = default!;
+    [Dependency] private readonly SharedObjectivesSystem _objectives = default!;
+    [Dependency] private readonly SharedPlayerSystem _player = default!;
 
     // This is dictionary is required to track the minds of disconnected players that may have had their entity deleted.
     protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
@@ -90,7 +92,7 @@ public abstract class SharedMindSystem : EntitySystem
         if (!mindContainer.ShowExamineInfo || !args.IsInDetailsRange)
             return;
 
-        var dead = _mobStateSystem.IsDead(uid);
+        var dead = _mobState.IsDead(uid);
         var hasSession = CompOrNull<MindComponent>(mindContainer.Mind)?.Session;
 
         if (dead && !mindContainer.HasMind)
@@ -166,7 +168,7 @@ public abstract class SharedMindSystem : EntitySystem
         if (targetMobState == null)
             return true;
         // They might actually be alive.
-        return _mobStateSystem.IsDead(mind.OwnedEntity.Value, targetMobState);
+        return _mobState.IsDead(mind.OwnedEntity.Value, targetMobState);
     }
 
     public virtual void Visit(EntityUid mindId, EntityUid entity, MindComponent? mind = null)
@@ -215,7 +217,7 @@ public abstract class SharedMindSystem : EntitySystem
 
     public void WipeMind(ICommonSession player)
     {
-        var mind = _playerSystem.ContentData(player)?.Mind;
+        var mind = _player.ContentData(player)?.Mind;
         DebugTools.Assert(GetMind(player.UserId) == mind);
         WipeMind(mind);
     }
@@ -251,59 +253,44 @@ public abstract class SharedMindSystem : EntitySystem
     }
 
     /// <summary>
-    /// Adds an objective to this mind.
+    /// Tries to create and add an objective from its prototype id.
     /// </summary>
-    public bool TryAddObjective(EntityUid mindId, MindComponent mind, ObjectivePrototype objectivePrototype)
+    /// <returns>Returns true if adding the objective succeeded.</returns>
+    public bool TryAddObjective(EntityUid mindId, MindComponent mind, string proto)
     {
-        if (!objectivePrototype.CanBeAssigned(mindId, mind))
+        var objective = _objectives.TryCreateObjective(mindId, mind, proto);
+        if (objective == null)
             return false;
-        var objective = objectivePrototype.GetObjective(mindId, mind);
-        if (mind.Objectives.Contains(objective))
-            return false;
-
-        foreach (var condition in objective.Conditions)
-        {
-            _adminLogger.Add(LogType.Mind, LogImpact.Low, $"'{condition.Title}' added to mind of {MindOwnerLoggingString(mind)}");
-        }
 
-        mind.Objectives.Add(objective);
+        AddObjective(mindId, mind, objective.Value);
         return true;
     }
 
     /// <summary>
-    /// Adds an objective, by id, to this mind.
+    /// Adds an objective that already exists, and is assumed to have had its requirements checked.
     /// </summary>
-    public bool TryAddObjective(EntityUid mindId, string name, MindComponent? mind = null)
+    public void AddObjective(EntityUid mindId, MindComponent mind, EntityUid objective)
     {
-        if (!Resolve(mindId, ref mind))
-            return false;
-
-        if (!_proto.TryIndex<ObjectivePrototype>(name, out var objective))
-        {
-            Log.Error($"Tried to add unknown objective prototype: {name}");
-            return false;
-        }
-
-        return TryAddObjective(mindId, mind, objective);
+        var title = Name(objective);
+        _adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) added to mind of {MindOwnerLoggingString(mind)}");
+        mind.Objectives.Add(objective);
     }
 
     /// <summary>
-    /// Removes an objective to this mind.
+    /// Removes an objective from this mind.
     /// </summary>
     /// <returns>Returns true if the removal succeeded.</returns>
-    public bool TryRemoveObjective(MindComponent mind, int index)
+    public bool TryRemoveObjective(EntityUid mindId, MindComponent mind, int index)
     {
         if (index < 0 || index >= mind.Objectives.Count)
             return false;
 
         var objective = mind.Objectives[index];
 
-        foreach (var condition in objective.Conditions)
-        {
-            _adminLogger.Add(LogType.Mind, LogImpact.Low, $"'{condition.Title}' removed from the mind of {MindOwnerLoggingString(mind)}");
-        }
-
+        var title = Name(objective);
+        _adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) removed from the mind of {MindOwnerLoggingString(mind)}");
         mind.Objectives.Remove(objective);
+        Del(objective);
         return true;
     }
 
@@ -356,7 +343,7 @@ public abstract class SharedMindSystem : EntitySystem
     {
         mindId = default;
         mind = null;
-        return _playerSystem.ContentData(player) is { } data && TryGetMind(data, out mindId, out mind);
+        return _player.ContentData(player) is { } data && TryGetMind(data, out mindId, out mind);
     }
 
     /// <summary>
@@ -432,6 +419,30 @@ public abstract class SharedMindSystem : EntitySystem
     {
         return TryGetMind(userId, out _, out var mind) ? mind.CharacterName : null;
     }
+
+    /// <summary>
+    /// Returns a list of every living humanoid player's minds, except for a single one which is exluded.
+    /// </summary>
+    public List<EntityUid> GetAliveHumansExcept(EntityUid exclude)
+    {
+        var mindQuery = EntityQuery<MindComponent>();
+
+        var allHumans = new List<EntityUid>();
+        // HumanoidAppearanceComponent is used to prevent mice, pAIs, etc from being chosen
+        var query = EntityQueryEnumerator<MindContainerComponent, MobStateComponent, HumanoidAppearanceComponent>();
+        while (query.MoveNext(out var uid, out var mc, out var mobState, out _))
+        {
+            // the player needs to have a mind and not be the excluded one
+            if (mc.Mind == null || mc.Mind == exclude)
+                continue;
+
+            // the player has to be alive
+            if (_mobState.IsAlive(uid, mobState))
+                allHumans.Add(mc.Mind.Value);
+        }
+
+        return allHumans;
+    }
 }
 
 /// <summary>
diff --git a/Content.Shared/Objectives/Components/ObjectiveComponent.cs b/Content.Shared/Objectives/Components/ObjectiveComponent.cs
new file mode 100644 (file)
index 0000000..95fbc68
--- /dev/null
@@ -0,0 +1,69 @@
+using Content.Shared.Mind;
+using Content.Shared.Objectives;
+using Content.Shared.Objectives.Systems;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Objectives.Components;
+
+/// <summary>
+/// Required component for an objective entity prototype.
+/// </summary>
+[RegisterComponent, Access(typeof(SharedObjectivesSystem))]
+public sealed partial class ObjectiveComponent : Component
+{
+    /// <summary>
+    /// Difficulty rating used to avoid assigning too many difficult objectives.
+    /// </summary>
+    [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
+    public float Difficulty;
+
+    /// <summary>
+    /// Organisation that issued this objective, used for grouping and as a header above common objectives.
+    /// </summary>
+    [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
+    public string Issuer = string.Empty;
+
+    /// <summary>
+    /// Unique objectives can only have 1 per prototype id.
+    /// Set this to false if you want multiple objectives of the same prototype.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public bool Unique = true;
+
+    /// <summary>
+    /// Icon of this objective to display in the character menu.
+    /// Can be specified by an <see cref="ObjectiveGetInfoEvent"/> handler but is usually done in the prototype.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public SpriteSpecifier? Icon;
+}
+
+/// <summary>
+/// Event raised on an objective after spawning it to see if it meets all the requirements.
+/// Requirement components should have subscriptions and cancel if the requirements are not met.
+/// If a requirement is not met then the objective is deleted.
+/// </summary>
+[ByRefEvent]
+public record struct RequirementCheckEvent(EntityUid MindId, MindComponent Mind, bool Cancelled = false);
+
+/// <summary>
+/// Event raised on an objective after its requirements have been checked.
+/// If <see cref="Cancelled"/> is set to true, the objective is deleted.
+/// Use this if the objective cannot be used, like a kill objective with no people alive.
+/// </summary>
+[ByRefEvent]
+public record struct ObjectiveAssignedEvent(EntityUid MindId, MindComponent Mind, bool Cancelled = false);
+
+/// <summary>
+/// Event raised on an objective after everything has handled <see cref="ObjectiveAssignedEvent"/>.
+/// Use this to set the objective's title description or icon.
+/// </summary>
+[ByRefEvent]
+public record struct ObjectiveAfterAssignEvent(EntityUid MindId, MindComponent Mind, ObjectiveComponent Objective, MetaDataComponent Meta);
+
+/// <summary>
+/// Event raised on an objective to update the Progress field.
+/// To use this yourself call <see cref="SharedObjectivesSystem.GetInfo"/> with the mind.
+/// </summary>
+[ByRefEvent]
+public record struct ObjectiveGetProgressEvent(EntityUid MindId, MindComponent Mind, float? Progress = null);
diff --git a/Content.Shared/Objectives/ConditionInfo.cs b/Content.Shared/Objectives/ConditionInfo.cs
deleted file mode 100644 (file)
index 3aa335c..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-using Robust.Shared.Serialization;
-using Robust.Shared.Utility;
-
-namespace Content.Shared.Objectives
-{
-    [Serializable, NetSerializable]
-    public sealed class ConditionInfo
-    {
-        public string Title { get; }
-        public string Description { get; }
-        public SpriteSpecifier SpriteSpecifier { get; }
-        public float Progress { get; }
-
-        public ConditionInfo(string title, string description, SpriteSpecifier spriteSpecifier, float progress)
-        {
-            Title = title;
-            Description = description;
-            SpriteSpecifier = spriteSpecifier;
-            Progress = progress;
-        }
-    }
-}
diff --git a/Content.Shared/Objectives/Interfaces/IObjectiveCondition.cs b/Content.Shared/Objectives/Interfaces/IObjectiveCondition.cs
deleted file mode 100644 (file)
index 79e77e1..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-using Content.Shared.Mind;
-using Robust.Shared.Utility;
-
-namespace Content.Shared.Objectives.Interfaces
-{
-    // TODO refactor all of this to be ecs
-    public interface IObjectiveCondition
-    {
-        /// <summary>
-        /// Returns a copy of the IObjectiveCondition which is assigned to the mind.
-        /// </summary>
-        /// <param name="mindId">Mind id to assign to.</param>
-        /// <param name="mind">Mind to assign to.</param>
-        /// <returns>The new IObjectiveCondition.</returns>
-        IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind);
-
-        /// <summary>
-        /// Returns the title of the condition.
-        /// </summary>
-        string Title { get; }
-
-        /// <summary>
-        /// Returns the description of the condition.
-        /// </summary>
-        string Description { get; }
-
-        /// <summary>
-        /// Returns a SpriteSpecifier to be used as an icon for the condition.
-        /// </summary>
-        SpriteSpecifier Icon { get; }
-
-        /// <summary>
-        /// Returns the current progress of the condition in % from 0 to 1.
-        /// </summary>
-        /// <returns>Current progress in %.</returns>
-        float Progress { get; }
-
-        /// <summary>
-        /// Returns a difficulty of the condition.
-        /// </summary>
-        float Difficulty { get; }
-    }
-}
diff --git a/Content.Shared/Objectives/Interfaces/IObjectiveRequirement.cs b/Content.Shared/Objectives/Interfaces/IObjectiveRequirement.cs
deleted file mode 100644 (file)
index 973a0ea..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-using Content.Shared.Mind;
-
-namespace Content.Shared.Objectives.Interfaces
-{
-    // TODO refactor all of this to be ecs
-    public interface IObjectiveRequirement
-    {
-        /// <summary>
-        /// Checks whether or not the entity & its surroundings are valid to be given the objective.
-        /// </summary>
-        /// <returns>Returns true if objective can be given.</returns>
-        bool CanBeAssigned(EntityUid mindId, MindComponent mind);
-    }
-}
diff --git a/Content.Shared/Objectives/Objective.cs b/Content.Shared/Objectives/Objective.cs
deleted file mode 100644 (file)
index 3f7b752..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-
-namespace Content.Shared.Objectives
-{
-    public sealed class Objective : IEquatable<Objective>
-    {
-        [ViewVariables]
-        public readonly EntityUid MindId;
-        [ViewVariables]
-        public readonly MindComponent Mind;
-        [ViewVariables]
-        public readonly ObjectivePrototype Prototype;
-        private readonly List<IObjectiveCondition> _conditions = new();
-        [ViewVariables]
-        public IReadOnlyList<IObjectiveCondition> Conditions => _conditions;
-
-        public Objective(ObjectivePrototype prototype, EntityUid mindId, MindComponent mind)
-        {
-            Prototype = prototype;
-            MindId = mindId;
-            Mind = mind;
-            foreach (var condition in prototype.Conditions)
-            {
-                _conditions.Add(condition.GetAssigned(mindId, mind));
-            }
-        }
-
-        public bool Equals(Objective? other)
-        {
-            if (other is null) return false;
-            if (ReferenceEquals(this, other)) return true;
-            if (!Equals(Mind, other.Mind) || !Equals(Prototype, other.Prototype)) return false;
-            if (_conditions.Count != other._conditions.Count) return false;
-            for (var i = 0; i < _conditions.Count; i++)
-            {
-                if (!_conditions[i].Equals(other._conditions[i])) return false;
-            }
-
-            return true;
-        }
-
-        public override bool Equals(object? obj)
-        {
-            if (ReferenceEquals(null, obj)) return false;
-            if (ReferenceEquals(this, obj)) return true;
-            if (obj.GetType() != GetType()) return false;
-            return Equals((Objective) obj);
-        }
-
-        public override int GetHashCode()
-        {
-            return HashCode.Combine(Mind, Prototype, _conditions);
-        }
-    }
-}
diff --git a/Content.Shared/Objectives/ObjectiveInfo.cs b/Content.Shared/Objectives/ObjectiveInfo.cs
new file mode 100644 (file)
index 0000000..689fe17
--- /dev/null
@@ -0,0 +1,17 @@
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Objectives;
+
+/// <summary>
+/// Info about objectives visible in the character menu and on round end.
+/// Description and icon are displayed only in the character menu.
+/// Progress is a percentage from 0.0 to 1.0.
+/// </summary>
+/// <remarks>
+/// All of these fields must eventually be set by condition event handlers.
+/// Everything but progress can be set to static data in yaml on the entity and <see cref="ObjectiveComponent"/>.
+/// If anything is null it will be logged and return null.
+/// </remarks>
+[Serializable, NetSerializable]
+public record struct ObjectiveInfo(string Title, string Description, SpriteSpecifier Icon, float Progress);
diff --git a/Content.Shared/Objectives/ObjectivePrototype.cs b/Content.Shared/Objectives/ObjectivePrototype.cs
deleted file mode 100644 (file)
index 286afb5..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-using System.Linq;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Interfaces;
-using Robust.Shared.Prototypes;
-
-namespace Content.Shared.Objectives
-{
-    /// <summary>
-    ///     Prototype for objectives. Remember that to be assigned, it should be added to one or more objective groups in prototype. E.g. crew, traitor, wizard
-    /// </summary>
-    [Prototype("objective")]
-    public sealed class ObjectivePrototype : IPrototype
-    {
-        [ViewVariables]
-        [IdDataField]
-        public string ID { get; private set; } = default!;
-
-        [DataField("issuer")] public string Issuer { get; private set; } = "Unknown";
-
-        [ViewVariables]
-        public float Difficulty => _difficultyOverride ?? _conditions.Sum(c => c.Difficulty);
-
-        [DataField("conditions", serverOnly: true)]
-        private List<IObjectiveCondition> _conditions = new();
-        [DataField("requirements")]
-        private List<IObjectiveRequirement> _requirements = new();
-
-        [ViewVariables]
-        public IReadOnlyList<IObjectiveCondition> Conditions => _conditions;
-
-        [DataField("canBeDuplicate")]
-        public bool CanBeDuplicateAssignment { get; private set; }
-
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("difficultyOverride")]
-        private float? _difficultyOverride = null;
-
-        public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
-        {
-            foreach (var requirement in _requirements)
-            {
-                if (!requirement.CanBeAssigned(mindId, mind))
-                    return false;
-            }
-
-            if (!CanBeDuplicateAssignment)
-            {
-                foreach (var objective in mind.AllObjectives)
-                {
-                    if (objective.Prototype.ID == ID)
-                        return false;
-                }
-            }
-
-            return true;
-        }
-
-        public Objective GetObjective(EntityUid mindId, MindComponent mind)
-        {
-            return new Objective(this, mindId, mind);
-        }
-    }
-}
diff --git a/Content.Shared/Objectives/Systems/SharedObjectivesSystem.cs b/Content.Shared/Objectives/Systems/SharedObjectivesSystem.cs
new file mode 100644 (file)
index 0000000..dffb4e7
--- /dev/null
@@ -0,0 +1,130 @@
+using Content.Shared.Mind;
+using Content.Shared.Objectives;
+using Content.Shared.Objectives.Components;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Objectives.Systems;
+
+/// <summary>
+/// Provides API for creating and interacting with objectives.
+/// </summary>
+public abstract class SharedObjectivesSystem : EntitySystem
+{
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+
+    private EntityQuery<MetaDataComponent> _metaQuery;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        _metaQuery = GetEntityQuery<MetaDataComponent>();
+    }
+
+    /// <summary>
+    /// Checks requirements and duplicate objectives to see if an objective can be assigned.
+    /// </summary>
+    public bool CanBeAssigned(EntityUid uid, EntityUid mindId, MindComponent mind, ObjectiveComponent? comp = null)
+    {
+        if (!Resolve(uid, ref comp))
+            return false;
+
+        var ev = new RequirementCheckEvent(mindId, mind);
+        RaiseLocalEvent(uid, ref ev);
+        if (ev.Cancelled)
+            return false;
+
+        // only check for duplicate prototypes if it's unique
+        if (comp.Unique)
+        {
+            var proto = _metaQuery.GetComponent(uid).EntityPrototype?.ID;
+            foreach (var objective in mind.AllObjectives)
+            {
+                if (_metaQuery.GetComponent(objective).EntityPrototype?.ID == proto)
+                    return false;
+            }
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    /// Spawns and assigns an objective for a mind.
+    /// The objective is not added to the mind's objectives, mind system does that in TryAddObjective.
+    /// If the objective could not be assigned the objective is deleted and null is returned.
+    /// </summary>
+    public EntityUid? TryCreateObjective(EntityUid mindId, MindComponent mind, string proto)
+    {
+        var uid = Spawn(proto);
+        if (!TryComp<ObjectiveComponent>(uid, out var comp))
+        {
+            Del(uid);
+            Log.Error($"Invalid objective prototype {proto}, missing ObjectiveComponent");
+            return null;
+        }
+
+        Log.Debug($"Created objective {proto} ({uid})");
+
+        if (!CanBeAssigned(uid, mindId, mind, comp))
+        {
+            Del(uid);
+            Log.Warning($"Objective {uid} did not match the requirements for {_mind.MindOwnerLoggingString(mind)}, deleted it");
+            return null;
+        }
+
+        var ev = new ObjectiveAssignedEvent(mindId, mind);
+        RaiseLocalEvent(uid, ref ev);
+        if (ev.Cancelled)
+        {
+            Del(uid);
+            Log.Warning($"Could not assign objective {uid}, deleted it");
+            return null;
+        }
+
+        // let the title description and icon be set by systems
+        var afterEv = new ObjectiveAfterAssignEvent(mindId, mind, comp, MetaData(uid));
+        RaiseLocalEvent(uid, ref afterEv);
+
+        return uid;
+    }
+
+    /// <summary>
+    /// Get the title, description, icon and progress of an objective using <see cref="ObjectiveGetInfoEvent"/>.
+    /// If any of them are null it is logged and null is returned.
+    /// </summary>
+    /// <param name="uid"/>ID of the condition entity</param>
+    /// <param name="mindId"/>ID of the player's mind entity</param>
+    /// <param name="mind"/>Mind component of the player's mind</param>
+    public ObjectiveInfo? GetInfo(EntityUid uid, EntityUid mindId, MindComponent? mind = null)
+    {
+        if (!Resolve(mindId, ref mind))
+            return null;
+
+        var ev = new ObjectiveGetProgressEvent(mindId, mind);
+        RaiseLocalEvent(uid, ref ev);
+
+        var comp = Comp<ObjectiveComponent>(uid);
+        var meta = MetaData(uid);
+        var title = meta.EntityName;
+        var description = meta.EntityDescription;
+        if (comp.Icon == null || ev.Progress == null)
+        {
+            Log.Error($"An objective {ToPrettyString(uid):objective} of {_mind.MindOwnerLoggingString(mind)} is missing icon or progress ({ev.Progress})");
+            return null;
+        }
+
+        return new ObjectiveInfo(title, description, comp.Icon, ev.Progress.Value);
+    }
+
+    /// <summary>
+    /// Sets the objective's icon to the one specified.
+    /// Intended for <see cref="ObjectiveAfterAssignEvent"/> handlers to set an icon.
+    /// </summary>
+    public void SetIcon(EntityUid uid, SpriteSpecifier icon, ObjectiveComponent? comp = null)
+    {
+        if (!Resolve(uid, ref comp))
+            return;
+
+        comp.Icon = icon;
+    }
+}
diff --git a/Resources/Locale/en-US/objectives/conditions/die-condition.ftl b/Resources/Locale/en-US/objectives/conditions/die-condition.ftl
deleted file mode 100644 (file)
index d00c3cc..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-objective-condition-die-title = Die a glorious death
-objective-condition-die-description = Die.
\ No newline at end of file
similarity index 73%
rename from Resources/Locale/en-US/objectives/conditions/doorjack-condition.ftl
rename to Resources/Locale/en-US/objectives/conditions/doorjack.ftl
index e2bed1cef8755b2ac3710ddd7420a15ff1736cb2..6e05fd279d994a6c65d2a47842140ba9707dcba0 100644 (file)
@@ -1,2 +1,2 @@
 objective-condition-doorjack-title = Doorjack {$count} doors on the station.
-objective-condition-doorjack-description = Your gloves can emag airlocks. Do this {$count} doors on the station.
+objective-condition-doorjack-description = Your gloves can emag airlocks. Do this to {$count} doors on the station.
diff --git a/Resources/Locale/en-US/objectives/conditions/escape-shuttle-condition.ftl b/Resources/Locale/en-US/objectives/conditions/escape-shuttle-condition.ftl
deleted file mode 100644 (file)
index 5f95044..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-objective-condition-escape-shuttle-title = Escape to centcom alive and unrestrained.
-objective-condition-escape-shuttle-description = One of our undercover agents will debrief you when you arrive. Don't show up in cuffs.
diff --git a/Resources/Locale/en-US/objectives/conditions/kill-head-condition.ftl b/Resources/Locale/en-US/objectives/conditions/kill-head-condition.ftl
deleted file mode 100644 (file)
index ad8861b..0000000
+++ /dev/null
@@ -1 +0,0 @@
-objective-condition-kill-head-description = We need this head gone and you probably know why. Good luck, agent.
diff --git a/Resources/Locale/en-US/objectives/conditions/kill-head.ftl b/Resources/Locale/en-US/objectives/conditions/kill-head.ftl
new file mode 100644 (file)
index 0000000..dce2a94
--- /dev/null
@@ -0,0 +1 @@
+objective-condition-kill-head-title = Kill {$targetName}, {CAPITALIZE($job)}
diff --git a/Resources/Locale/en-US/objectives/conditions/kill-person-condition.ftl b/Resources/Locale/en-US/objectives/conditions/kill-person-condition.ftl
deleted file mode 100644 (file)
index 8d25f44..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-objective-condition-kill-person-title = Kill or maroon {$targetName}, {CAPITALIZE($job)}
-objective-condition-kill-person-description = Do it however you like, just make sure they don't make it to centcom.
diff --git a/Resources/Locale/en-US/objectives/conditions/kill-person.ftl b/Resources/Locale/en-US/objectives/conditions/kill-person.ftl
new file mode 100644 (file)
index 0000000..c48e212
--- /dev/null
@@ -0,0 +1 @@
+objective-condition-kill-person-title = Kill or maroon {$targetName}, {CAPITALIZE($job)}
similarity index 50%
rename from Resources/Locale/en-US/objectives/conditions/other-traitor-alive-condition.ftl
rename to Resources/Locale/en-US/objectives/conditions/other-traitor-alive.ftl
index 54ddc31c10b129e7f6e19bad483a75495f44adc0..524d0dde63cde9a02bc4725a8afcc5c0a8336e3e 100644 (file)
@@ -1,2 +1 @@
 objective-condition-other-traitor-alive-title = Ensure fellow traitor {$targetName}, {CAPITALIZE($job)} stays alive.
-objective-condition-other-traitor-alive-description = Identify yourself at your own risk. We just need them alive.
similarity index 54%
rename from Resources/Locale/en-US/objectives/conditions/other-traitor-progress-condition.ftl
rename to Resources/Locale/en-US/objectives/conditions/other-traitor-progress.ftl
index 883e61ce4f4f8be9f5a0ee1eb9df6137e9639402..4ee832d7ce0f6e9cff9ff91c5226fcbdd1660862 100644 (file)
@@ -1,2 +1 @@
 objective-condition-other-traitor-progress-title = Ensure fellow traitor {$targetName}, {CAPITALIZE($job)} achieves at least half their objectives.
-objective-condition-other-traitor-progress-description = Identify yourself at your own risk. We just need them to succeed.
diff --git a/Resources/Locale/en-US/objectives/conditions/spider-charge-condition.ftl b/Resources/Locale/en-US/objectives/conditions/spider-charge-condition.ftl
deleted file mode 100644 (file)
index 3ce7a98..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-objective-condition-spider-charge-title = Detonate the spider clan charge in {$location}
-objective-condition-spider-charge-no-target = Detonate the spider clan charge... somewhere?
-objective-condition-spider-charge-description = This bomb can be detonated in a specific location. Note that the bomb will not work anywhere else!
diff --git a/Resources/Locale/en-US/objectives/conditions/spider-charge.ftl b/Resources/Locale/en-US/objectives/conditions/spider-charge.ftl
new file mode 100644 (file)
index 0000000..cdc3cfd
--- /dev/null
@@ -0,0 +1 @@
+objective-condition-spider-charge-title = Detonate the spider clan charge in {$location}
diff --git a/Resources/Locale/en-US/objectives/conditions/steal-research-condition.ftl b/Resources/Locale/en-US/objectives/conditions/steal-research-condition.ftl
deleted file mode 100644 (file)
index a9a820c..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-objective-condition-steal-research-title = Steal {$count} technologies.
-objective-condition-steal-research-description = Your gloves can be used to hack a research server and steal its precious data. If science has been slacking you'll have to get to work.
diff --git a/Resources/Locale/en-US/objectives/conditions/steal-research.ftl b/Resources/Locale/en-US/objectives/conditions/steal-research.ftl
new file mode 100644 (file)
index 0000000..fe5b746
--- /dev/null
@@ -0,0 +1 @@
+objective-condition-steal-research-title = Steal {$count} technologies.
diff --git a/Resources/Locale/en-US/objectives/conditions/survive-condition.ftl b/Resources/Locale/en-US/objectives/conditions/survive-condition.ftl
deleted file mode 100644 (file)
index 5c9115a..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-objective-condition-survive-title = Survive
-objective-condition-survive-description = You wouldn't be a very good ninja if you died, now would you?
diff --git a/Resources/Locale/en-US/objectives/conditions/terror-condition.ftl b/Resources/Locale/en-US/objectives/conditions/terror-condition.ftl
deleted file mode 100644 (file)
index 104f578..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-objective-condition-terror-title = Call in a threat
-objective-condition-terror-description = Use your gloves on a communication console in order to bring another threat to the station.
index c5ef2aacffb5133a7d1961ede7bc8ba45478c61b..4c0e5884ca31506ba2fc02c150a8f31be8ce82dc 100644 (file)
@@ -10,5 +10,5 @@ objectives-player-named = [color=White]{$name}[/color]
 objectives-no-objectives = {$title} was a {$agent}.
 objectives-with-objectives = {$title} was a {$agent} who had the following objectives:
 
-objectives-condition-success = {$condition} | [color={$markupColor}]Success![/color]
-objectives-condition-fail = {$condition} | [color={$markupColor}]Failure![/color] ({$progress}%)
+objectives-objective-success = {$objective} | [color={$markupColor}]Success![/color]
+objectives-objective-fail = {$objective} | [color={$markupColor}]Failure![/color] ({$progress}%)
index 0d5983b178b2e77ccdf8cbe134f9d857010002e9..6d431a9dcd19852bbbcc9200cdd5dc7ea2fccd74 100644 (file)
@@ -11,7 +11,7 @@
     - DoorjackObjective
     - SpiderChargeObjective
     - TerrorObjective
-    - SurviveObjective
+    - NinjaSurviveObjective
     threats:
     - announcement: terror-dragon
       rule: Dragon
diff --git a/Resources/Prototypes/Objectives/base_objectives.yml b/Resources/Prototypes/Objectives/base_objectives.yml
new file mode 100644 (file)
index 0000000..b4ce16a
--- /dev/null
@@ -0,0 +1,101 @@
+# OBJECTIVE STYLE
+# in comments anything that says final prototype means the objective that isnt abstract
+# the final prototype must be noSpawn to avoid showing in f5
+# components are listed in this order:
+# 1. Objective
+# 2. requirement components
+# 3. non-condition components
+# 4. the condition component
+
+# all objectives should inherit this at some point
+# then have its difficulty etc fields set in the final objective prototypes
+- type: entity
+  abstract: true
+  id: BaseObjective
+  components:
+  - type: Objective
+
+# requires that the player not have a die objective
+- type: entity
+  abstract: true
+  parent: BaseObjective
+  id: BaseLivingObjective
+  components:
+  - type: ObjectiveBlacklistRequirement
+    blacklist:
+      components:
+      - DieCondition
+
+# objective that targets a player
+# final prototype must specify the title locale id in TargetObjective
+- type: entity
+  abstract: true
+  parent: BaseObjective
+  id: BaseTargetObjective
+  components:
+  - type: TargetObjective
+
+# requires that the player kill someone
+# disables social objectives and is disabled by social objectives
+- type: entity
+  abstract: true
+  parent: BaseTargetObjective
+  id: BaseKillObjective
+  components:
+  - type: Objective
+    unique: false
+    icon:
+      sprite: Objects/Weapons/Guns/Pistols/viper.rsi
+      state: icon
+  - type: ObjectiveBlacklistRequirement
+    blacklist:
+      components:
+      - SocialObjective
+  - type: KillPersonCondition
+
+# requires that the player interact socially with someone
+# disables kill objectives and is disabled by kill objectives
+- type: entity
+  abstract: true
+  parent: BaseTargetObjective
+  id: BaseSocialObjective
+  components:
+  - type: Objective
+    unique: false
+  - type: ObjectiveBlacklistRequirement
+    blacklist:
+      components:
+      - KillPersonCondition
+  - type: SocialObjective
+
+# requires that the target survives the round
+- type: entity
+  abstract: true
+  parent: BaseSocialObjective
+  id: BaseKeepAliveObjective
+  components:
+  - type: KeepAliveCondition
+
+# requires that the target completes at least 50% of their objectives
+- type: entity
+  abstract: true
+  parent: BaseSocialObjective
+  id: BaseHelpProgressObjective
+  components:
+  - type: HelpProgressCondition
+
+# requires that the player steal an item specified in the final prototype
+- type: entity
+  abstract: true
+  parent: BaseLivingObjective
+  id: BaseStealObjective
+  components:
+  - type: StealCondition
+
+# requires that the player not die, ignores being on emergency shuttle or cuffed
+- type: entity
+  abstract: true
+  parent: BaseObjective
+  id: BaseSurviveObjective
+  components:
+  - type: SurviveCondition
diff --git a/Resources/Prototypes/Objectives/ninja.yml b/Resources/Prototypes/Objectives/ninja.yml
new file mode 100644 (file)
index 0000000..a729164
--- /dev/null
@@ -0,0 +1,83 @@
+- type: entity
+  abstract: true
+  parent: BaseObjective
+  id: BaseNinjaObjective
+  components:
+  - type: Objective
+    # difficulty isn't used all since objectives are picked
+    difficulty: 1.5
+    issuer: spiderclan
+  - type: RoleRequirement
+    roles:
+      components:
+      - NinjaRole
+
+- type: entity
+  noSpawn: true
+  parent: BaseNinjaObjective
+  id: DoorjackObjective
+  components:
+  - type: Objective
+    icon:
+      sprite: Objects/Tools/emag.rsi
+      state: icon
+  - type: NumberObjective
+    min: 15
+    max: 40
+    title: objective-condition-doorjack-title
+    description: objective-condition-doorjack-description
+  - type: DoorjackCondition
+
+- type: entity
+  noSpawn: true
+  parent: BaseNinjaObjective
+  id: StealResearchObjective
+  description: Your gloves can be used to hack a research server and steal its precious data. If science has been slacking you'll have to get to work.
+  components:
+  - type: Objective
+    icon:
+      sprite: Structures/Machines/server.rsi
+      state: server
+  - type: NumberObjective
+    min: 5
+    max: 10
+    title: objective-condition-steal-research-title
+  - type: StealResearchCondition
+
+- type: entity
+  noSpawn: true
+  parent: BaseNinjaObjective
+  id: SpiderChargeObjective
+  description: This bomb can be detonated in a specific location. Note that the bomb will not work anywhere else!
+  components:
+  - type: Objective
+    icon:
+      sprite: Objects/Weapons/Bombs/spidercharge.rsi
+      state: icon
+  - type: SpiderChargeTargetRequirement
+  - type: SpiderChargeCondition
+
+- type: entity
+  noSpawn: true
+  parent: [BaseNinjaObjective, BaseSurviveObjective]
+  id: NinjaSurviveObjective
+  name: Survive
+  description: You wouldn't be a very good ninja if you died, now would you?
+  components:
+  - type: Objective
+    icon:
+      sprite: Clothing/Mask/ninja.rsi
+      state: icon
+
+- type: entity
+  noSpawn: true
+  parent: BaseNinjaObjective
+  id: TerrorObjective
+  name: Call in a threat
+  description: Use your gloves on a communication console in order to bring another threat to the station.
+  components:
+  - type: Objective
+    icon:
+      sprite: Objects/Fun/Instruments/otherinstruments.rsi
+      state: red_phone
+  - type: TerrorCondition
diff --git a/Resources/Prototypes/Objectives/ninjaObjectives.yml b/Resources/Prototypes/Objectives/ninjaObjectives.yml
deleted file mode 100644 (file)
index f3df853..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-- type: objective
-  id: StealResearchObjective
-  issuer: spiderclan
-  requirements:
-  - !type:NinjaRequirement {}
-  conditions:
-  - !type:StealResearchCondition {}
-
-- type: objective
-  id: DoorjackObjective
-  issuer: spiderclan
-  requirements:
-  - !type:NinjaRequirement {}
-  conditions:
-  - !type:DoorjackCondition {}
-
-- type: objective
-  id: SpiderChargeObjective
-  issuer: spiderclan
-  requirements:
-  - !type:NinjaRequirement {}
-  - !type:SpiderChargeTargetRequirement {}
-  conditions:
-  - !type:SpiderChargeCondition {}
-
-- type: objective
-  id: TerrorObjective
-  issuer: spiderclan
-  requirements:
-  - !type:NinjaRequirement {}
-  conditions:
-  - !type:TerrorCondition {}
-
-- type: objective
-  id: SurviveObjective
-  issuer: spiderclan
-  requirements:
-  - !type:NinjaRequirement {}
-  conditions:
-  - !type:SurviveCondition {}
index 1c4034a8543a928398b436233fa8cab7e6fb7b76..01f3dd410946608bcf2d214d55b1928f6ce8ccc2 100644 (file)
@@ -24,7 +24,7 @@
 - type: weightedRandom
   id: TraitorObjectiveGroupKill
   weights:
-    KillRandomObjective: 1
+    KillRandomPersonObjective: 1
     KillRandomHeadObjective: 0.25
 
 - type: weightedRandom
diff --git a/Resources/Prototypes/Objectives/traitor.yml b/Resources/Prototypes/Objectives/traitor.yml
new file mode 100644 (file)
index 0000000..9ef34b8
--- /dev/null
@@ -0,0 +1,263 @@
+- type: entity
+  abstract: true
+  parent: BaseObjective
+  id: BaseTraitorObjective
+  components:
+  - type: Objective
+    issuer: syndicate
+  - type: RoleRequirement
+    roles:
+      components:
+      - TraitorRole
+
+- type: entity
+  abstract: true
+  parent: [BaseTraitorObjective, BaseSocialObjective]
+  id: BaseTraitorSocialObjective
+  components:
+  - type: Objective
+    icon:
+      sprite: Objects/Misc/bureaucracy.rsi
+      state: folder-white
+  - type: MultipleTraitorsRequirement
+
+- type: entity
+  abstract: true
+  parent: [BaseTraitorObjective, BaseStealObjective]
+  id: BaseTraitorStealObjective
+  components:
+  - type: Objective
+    difficulty: 2.75
+
+# state
+
+- type: entity
+  noSpawn: true
+  parent: [BaseTraitorObjective, BaseLivingObjective]
+  id: EscapeShuttleObjective
+  name: Escape to centcom alive and unrestrained.
+  description: One of our undercover agents will debrief you when you arrive. Don't show up in cuffs.
+  components:
+  - type: Objective
+    difficulty: 1.3
+    icon:
+      sprite: Structures/Furniture/chairs.rsi
+      state: shuttle
+  - type: EscapeShuttleCondition
+
+- type: entity
+  noSpawn: true
+  parent: BaseTraitorObjective
+  id: DieObjective
+  name: Die a glorious death
+  description: Die.
+  components:
+  - type: Objective
+    difficulty: 0.5
+    icon:
+      sprite: Mobs/Ghosts/ghost_human.rsi
+      state: icon
+  - type: ObjectiveBlacklistRequirement
+    blacklist:
+      components:
+      - EscapeShuttleCondition
+      - StealCondition
+  - type: DieCondition
+
+# kill
+
+- type: entity
+  noSpawn: true
+  parent: [BaseTraitorObjective, BaseKillObjective]
+  id: KillRandomPersonObjective
+  description: Do it however you like, just make sure they don't make it to centcom.
+  components:
+  - type: Objective
+    difficulty: 1.75
+    unique: false
+  - type: TargetObjective
+    title: objective-condition-kill-person-title
+  - type: PickRandomPerson
+
+- type: entity
+  noSpawn: true
+  parent: [BaseTraitorObjective, BaseKillObjective]
+  id: KillRandomHeadObjective
+  description: We need this head gone and you probably know why. Good luck, agent.
+  components:
+  - type: Objective
+    # technically its still possible for KillRandomPersonObjective to roll a head but this is guaranteed, so higher difficulty
+    difficulty: 3.0
+    # killing 1 head is enough
+    unique: true
+  - type: TargetObjective
+    title: objective-condition-kill-head-title
+  - type: PickRandomHead
+  - type: KillPersonCondition
+    # don't count missing evac as killing as heads are higher profile, so you really need to do the dirty work
+    # if ce flies a shittle to centcom you better find a way onto it
+    requireDead: true
+
+# social
+
+- type: entity
+  noSpawn: true
+  parent: [BaseTraitorSocialObjective, BaseKeepAliveObjective]
+  id: RandomTraitorAliveObjective
+  description: Identify yourself at your own risk. We just need them alive.
+  components:
+  - type: Objective
+    difficulty: 1.75
+  - type: TargetObjective
+    title: objective-condition-other-traitor-alive-title
+  - type: RandomTraitorAlive
+
+- type: entity
+  noSpawn: true
+  parent: [BaseTraitorSocialObjective, BaseHelpProgressObjective]
+  id: RandomTraitorProgressObjective
+  description: Identify yourself at your own risk. We just need them to succeed.
+  components:
+  - type: Objective
+    difficulty: 2.5
+  - type: TargetObjective
+    title: objective-condition-other-traitor-progress-title
+  - type: RandomTraitorProgress
+
+# steal
+
+## cmo
+
+- type: entity
+  noSpawn: true
+  parent: BaseTraitorStealObjective
+  id: HyposprayStealObjective
+  components:
+  - type: NotJobRequirement
+    job: ChiefMedicalOfficer
+  - type: StealCondition
+    prototype: Hypospray
+    owner: job-name-cmo
+
+## rd
+
+- type: entity
+  abstract: true
+  parent: BaseTraitorStealObjective
+  id: BaseRDObjective
+  components:
+  - type: NotJobRequirement
+    job: ResearchDirector
+  - type: StealCondition
+    owner: job-name-rd
+
+- type: entity
+  noSpawn: true
+  parent: BaseRDObjective
+  id: RDHardsuitStealObjective
+  components:
+  - type: StealCondition
+    prototype: ClothingOuterHardsuitRd
+
+- type: entity
+  noSpawn: true
+  parent: BaseRDObjective
+  id: HandTeleporterStealObjective
+  components:
+  - type: StealCondition
+    prototype: HandTeleporter
+
+## hos
+
+- type: entity
+  noSpawn: true
+  parent: BaseTraitorStealObjective
+  id: SecretDocumentsStealObjective
+  components:
+  - type: Objective
+    # hos has a gun ce does not, higher difficulty than most
+    difficulty: 3.5
+  - type: NotJobRequirement
+    job: HeadOfSecurity
+  - type: StealCondition
+    prototype: BookSecretDocuments
+    owner: job-name-hos
+
+## ce
+
+- type: entity
+  noSpawn: true
+  parent: BaseTraitorStealObjective
+  id: MagbootsStealObjective
+  components:
+  - type: NotJobRequirement
+    job: ChiefEngineer
+  - type: StealCondition
+    prototype: ClothingShoesBootsMagAdv
+    owner: job-name-ce
+
+## hop
+
+- type: entity
+  noSpawn: true
+  parent: BaseTraitorStealObjective
+  id: CorgiMeatStealObjective
+  components:
+  - type: NotJobRequirement
+    job: HeadOfPersonnel
+  - type: StealCondition
+    prototype: FoodMeatCorgi
+    owner: objective-condition-steal-Ian
+
+## cap
+
+- type: entity
+  abstract: true
+  parent: BaseTraitorStealObjective
+  id: BaseCaptainObjective
+  components:
+  - type: Objective
+    # sorry ce but your jordans are not as high security as the caps gear
+    difficulty: 3.5
+  - type: NotJobRequirement
+    job: Captain
+
+- type: entity
+  noSpawn: true
+  parent: BaseCaptainObjective
+  id: CaptainIDStealObjective
+  components:
+  - type: StealCondition
+    prototype: CaptainIDCard
+
+- type: entity
+  noSpawn: true
+  parent: BaseCaptainObjective
+  id: CaptainJetpackStealObjective
+  components:
+  - type: StealCondition
+    prototype: JetpackCaptainFilled
+
+- type: entity
+  noSpawn: true
+  parent: BaseCaptainObjective
+  id: CaptainGunStealObjective
+  components:
+  - type: StealCondition
+    prototype: WeaponAntiqueLaser
+    owner: job-name-captain
+
+- type: entity
+  noSpawn: true
+  parent: BaseCaptainObjective
+  id: StealNukeDiskObjective
+  components:
+  - type: Objective
+    # high difficulty since the hardest item both to steal, and to not get caught down the road,
+    # since anyone with a pinpointer can track you down and kill you
+    # it's close to being a stealth loneop
+    difficulty: 4.5
+  - type: NotCommandRequirement
+  - type: StealCondition
+    prototype: NukeDisk
+    owner: objective-condition-steal-station
diff --git a/Resources/Prototypes/Objectives/traitorObjectives.yml b/Resources/Prototypes/Objectives/traitorObjectives.yml
deleted file mode 100644 (file)
index a47d30f..0000000
+++ /dev/null
@@ -1,239 +0,0 @@
-- type: objective
-  id: CaptainIDStealObjective
-  issuer: syndicate
-  difficultyOverride: 2.75
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-      conditions:
-        - DieCondition
-    - !type:NotRoleRequirement
-      roleId: Captain
-  conditions:
-    - !type:StealCondition
-      prototype: CaptainIDCard
-
-- type: objective
-  id: KillRandomObjective
-  issuer: syndicate
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-        conditions:
-          - RandomTraitorAliveCondition
-  conditions:
-    - !type:KillRandomPersonCondition {}
-  canBeDuplicate: true
-
-# technically its still possible for KillRandomObjective to roll a head but this is guaranteed, so higher difficulty
-# this also will not count missing evac as killing as heads are higher profile, so you really need to do the dirty work
-- type: objective
-  id: KillRandomHeadObjective
-  issuer: syndicate
-  difficultyOverride: 3.0
-  requirements:
-  - !type:TraitorRequirement {}
-  - !type:IncompatibleConditionsRequirement
-    conditions:
-    - RandomTraitorAliveCondition
-  conditions:
-  - !type:KillRandomHeadCondition {}
-  # killing 1 head is enough
-  canBeDuplicate: false
-
-- type: objective
-  id: RandomTraitorAliveObjective
-  issuer: syndicate
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-        conditions:
-          - KillRandomPersonCondition
-    - !type:MultipleTraitorsRequirement
-  conditions:
-    - !type:RandomTraitorAliveCondition {}
-  canBeDuplicate: true
-
-- type: objective
-  id: DieObjective
-  issuer: syndicate
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-      conditions:
-        - StealCondition
-        - EscapeShuttleCondition
-  conditions:
-    - !type:DieCondition {}
-
-- type: objective
-  id: CMOHyposprayStealObjective
-  issuer: syndicate
-  difficultyOverride: 2.75
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-      conditions:
-        - DieCondition
-    - !type:NotRoleRequirement
-      roleId: ChiefMedicalOfficer
-  conditions:
-    - !type:StealCondition
-      prototype: Hypospray
-      owner: job-name-cmo
-
-- type: objective
-  id: RDHardsuitStealObjective
-  issuer: syndicate
-  difficultyOverride: 2.75
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-      conditions:
-        - DieCondition
-    - !type:NotRoleRequirement
-      roleId: ResearchDirector
-  conditions:
-    - !type:StealCondition
-      prototype: ClothingOuterHardsuitRd
-      owner: job-name-rd
-
-- type: objective
-  id: HandTeleporterStealObjective
-  issuer: syndicate
-  difficultyOverride: 2.75
-  requirements:
-  - !type:TraitorRequirement {}
-  - !type:IncompatibleConditionsRequirement
-    conditions:
-    - DieCondition
-  - !type:NotRoleRequirement
-    roleId: ResearchDirector
-  conditions:
-  - !type:StealCondition
-    prototype: HandTeleporter
-    owner: job-name-rd
-
-- type: objective
-  id: SecretDocumentsStealObjective
-  issuer: syndicate
-  difficultyOverride: 2.75
-  requirements:
-  - !type:TraitorRequirement {}
-  - !type:IncompatibleConditionsRequirement
-    conditions:
-    - DieCondition
-  - !type:NotRoleRequirement
-    roleId: HeadOfSecurity
-  conditions:
-  - !type:StealCondition
-    prototype: BookSecretDocuments
-    owner: job-name-hos
-
-- type: objective
-  id: NukeDiskStealObjective
-  issuer: syndicate
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-      conditions:
-        - DieCondition
-    - !type:NotRoleRequirement
-      roleId: Captain
-    - !type:NotRoleRequirement
-      roleId: HeadOfSecurity
-    - !type:NotRoleRequirement
-      roleId: HeadOfPersonnel
-    - !type:NotRoleRequirement
-      roleId: ChiefEngineer
-    - !type:NotRoleRequirement
-      roleId: ChiefMedicalOfficer
-    - !type:NotRoleRequirement
-      roleId: ResearchDirector
-  conditions:
-    - !type:StealCondition
-      prototype: NukeDisk
-      owner: objective-condition-steal-station
-
-- type: objective
-  id: MagbootsStealObjective
-  issuer: syndicate
-  difficultyOverride: 2.75
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-      conditions:
-        - DieCondition
-    - !type:NotRoleRequirement
-      roleId: ChiefEngineer
-  conditions:
-    - !type:StealCondition
-      prototype: ClothingShoesBootsMagAdv
-      owner: job-name-ce
-
-- type: objective
-  id: CorgiMeatStealObjective
-  issuer: syndicate
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-      conditions:
-        - DieCondition
-    - !type:NotRoleRequirement
-      roleId: HeadOfPersonnel
-  conditions:
-    - !type:StealCondition
-      prototype: FoodMeatCorgi
-      owner: objective-condition-steal-Ian
-
-- type: objective
-  id: CaptainGunStealObjective
-  issuer: syndicate
-  difficultyOverride: 2.75
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-      conditions:
-        - DieCondition
-    - !type:NotRoleRequirement
-      roleId: Captain
-  conditions:
-    - !type:StealCondition
-      prototype: WeaponAntiqueLaser
-      owner: job-name-captain
-
-- type: objective
-  id: CaptainJetpackStealObjective
-  issuer: syndicate
-  difficultyOverride: 2.75
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-      conditions:
-        - DieCondition
-    - !type:NotRoleRequirement
-      roleId: Captain
-  conditions:
-    - !type:StealCondition
-      prototype: JetpackCaptainFilled
-
-- type: objective
-  id: EscapeShuttleObjective
-  issuer: syndicate
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:IncompatibleConditionsRequirement
-      conditions:
-        - DieCondition
-  conditions:
-    - !type:EscapeShuttleCondition {}
-
-- type: objective
-  id: RandomTraitorProgressObjective
-  issuer: syndicate
-  requirements:
-    - !type:TraitorRequirement {}
-    - !type:MultipleTraitorsRequirement
-  conditions:
-    - !type:RandomTraitorProgressCondition {}
-  canBeDuplicate: true