diff --git a/Content.Client/CharacterInfo/CharacterInfoSystem.cs b/Content.Client/CharacterInfo/CharacterInfoSystem.cs index cd96085d31..93bd86d140 100644 --- a/Content.Client/CharacterInfo/CharacterInfoSystem.cs +++ b/Content.Client/CharacterInfo/CharacterInfoSystem.cs @@ -59,7 +59,7 @@ public sealed class CharacterInfoSystem : EntitySystem public readonly record struct CharacterData( EntityUid Entity, string Job, - Dictionary> Objectives, + Dictionary> 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 index 0000000000..8631ff2c11 --- /dev/null +++ b/Content.Client/Objectives/Systems/ObjectivesSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Objectives.Systems; + +namespace Content.Client.Objectives.Systems; + +public sealed class ObjectivesSystem : SharedObjectivesSystem +{ +} diff --git a/Content.Client/UserInterface/Systems/Character/CharacterUIController.cs b/Content.Client/UserInterface/Systems/Character/CharacterUIController.cs index beb37029c6..925b2dae4f 100644 --- a/Content.Client/UserInterface/Systems/Character/CharacterUIController.cs +++ b/Content.Client/UserInterface/Systems/Character/CharacterUIController.cs @@ -128,7 +128,7 @@ public sealed class CharacterUIController : UIController, IOnStateEntered>(); + var objectives = new Dictionary>(); 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(); - 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(objective).Issuer; + if (!objectives.ContainsKey(issuer)) + objectives[issuer] = new List(); + 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); } } diff --git a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs index 62eee90d50..e0789ab753 100644 --- a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs @@ -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 Minds = new(); /// - /// List of objective prototype ids to add + /// List of objective entity prototypes to add /// - [DataField("objectives", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer))] + [DataField("objectives", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer))] public List Objectives = new(); /// diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index 210425526c..01317dbfc1 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -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 if (objective == null) continue; - if (_mindSystem.TryAddObjective(mindId, mind, objective)) - difficulty += objective.Difficulty; + _mindSystem.AddObjective(mindId, mind, objective.Value); + difficulty += Comp(objective.Value).Difficulty; } } diff --git a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs index 4d856f7fb3..68827f956c 100644 --- a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs +++ b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs @@ -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}"); } diff --git a/Content.Server/Objectives/Commands/AddObjectiveCommand.cs b/Content.Server/Objectives/Commands/AddObjectiveCommand.cs index f5519e5095..f15dbf370c 100644 --- a/Content.Server/Objectives/Commands/AddObjectiveCommand.cs +++ b/Content.Server/Objectives/Commands/AddObjectiveCommand.cs @@ -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() - .TryIndex(args[1], out var objectivePrototype)) + .TryIndex(args[1], out var proto) || + !proto.TryGetComponent(out _)) { - shell.WriteLine($"Can't find matching ObjectivePrototype {objectivePrototype}"); + shell.WriteLine($"Can't find matching objective prototype {args[1]}"); return; } - var mindSystem = _entityManager.System(); - 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."); } } } diff --git a/Content.Server/Objectives/Commands/ListObjectivesCommand.cs b/Content.Server/Objectives/Commands/ListObjectivesCommand.cs index d1a7feb0ca..93dec3fa44 100644 --- a/Content.Server/Objectives/Commands/ListObjectivesCommand.cs +++ b/Content.Server/Objectives/Commands/ListObjectivesCommand.cs @@ -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(); - 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(); 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}%)"); + } } } diff --git a/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs b/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs index 26369822fd..b174ca94b6 100644 --- a/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs +++ b/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs @@ -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(); - 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 index 0000000000..bd928ae140 --- /dev/null +++ b/Content.Server/Objectives/Components/DieConditionComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that the player dies to be complete. +/// +[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 index 0000000000..714a70d8b9 --- /dev/null +++ b/Content.Server/Objectives/Components/DoorjackConditionComponent.cs @@ -0,0 +1,12 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Objective condition that requires the player to be a ninja and have doorjacked at least a random number of airlocks. +/// Requires to function. +/// +[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 index 0000000000..df5c3d1e03 --- /dev/null +++ b/Content.Server/Objectives/Components/EscapeShuttleConditionComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that the player is on the emergency shuttle's grid when docking to CentCom. +/// +[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 index 0000000000..df159a4dbc --- /dev/null +++ b/Content.Server/Objectives/Components/HelpProgressConditionComponent.cs @@ -0,0 +1,12 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that a target completes half of their objectives. +/// Depends on to function. +/// +[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 index 0000000000..4d287b9d83 --- /dev/null +++ b/Content.Server/Objectives/Components/KeepAliveConditionComponent.cs @@ -0,0 +1,12 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that a target stays alive. +/// Depends on to function. +/// +[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 index 0000000000..7bbc42ac98 --- /dev/null +++ b/Content.Server/Objectives/Components/KillPersonConditionComponent.cs @@ -0,0 +1,17 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that a target dies or, if is false, is not on the emergency shuttle. +/// Depends on to function. +/// +[RegisterComponent, Access(typeof(KillPersonConditionSystem))] +public sealed partial class KillPersonConditionComponent : Component +{ + /// + /// Whether the target must be truly dead, ignores missing evac. + /// + [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 index 0000000000..cb8202d07b --- /dev/null +++ b/Content.Server/Objectives/Components/MultipleTraitorsRequirementComponent.cs @@ -0,0 +1,16 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that there are a certain number of other traitors alive for this objective to be given. +/// +[RegisterComponent, Access(typeof(MultipleTraitorsRequirementSystem))] +public sealed partial class MultipleTraitorsRequirementComponent : Component +{ + /// + /// Number of traitors, excluding yourself, that have to exist. + /// + [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 index 0000000000..bc7520ef7e --- /dev/null +++ b/Content.Server/Objectives/Components/NotCommandRequirementComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that the player is not a member of command. +/// +[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 index 0000000000..6f6619da2b --- /dev/null +++ b/Content.Server/Objectives/Components/NotJobRequirementComponent.cs @@ -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; + +/// +/// Requires that the player not have a certain job to have this objective. +/// +[RegisterComponent, Access(typeof(NotJobRequirementSystem))] +public sealed partial class NotJobRequirementComponent : Component +{ + /// + /// ID of the job to ban from having this objective. + /// + [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + 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 index 0000000000..d4f4508b58 --- /dev/null +++ b/Content.Server/Objectives/Components/NumberObjectiveComponent.cs @@ -0,0 +1,41 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Objective has a target number of something. +/// When the objective is assigned it randomly picks this target from a minimum to a maximum. +/// +[RegisterComponent, Access(typeof(NumberObjectiveSystem))] +public sealed partial class NumberObjectiveComponent : Component +{ + /// + /// Number to use in the objective condition. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public int Target; + + /// + /// Minimum number for target to roll. + /// + [DataField(required: true)] + public int Min; + + /// + /// Maximum number for target to roll. + /// + [DataField(required: true)] + public int Max; + + /// + /// Optional title locale id, passed "count" with . + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public string? Title; + + /// + /// Optional description locale id, passed "count" with . + /// + [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 index 0000000000..02475af080 --- /dev/null +++ b/Content.Server/Objectives/Components/ObjectiveBlacklistRequirementComponent.cs @@ -0,0 +1,15 @@ +using Content.Server.Objectives.Systems; +using Content.Shared.Whitelist; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that the objective entity has no blacklisted components. +/// Lets you check for incompatible objectives. +/// +[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 index 0000000000..c2f82fb6c5 --- /dev/null +++ b/Content.Server/Objectives/Components/PickRandomHeadComponent.cs @@ -0,0 +1,12 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Sets the target for to a random head. +/// If there are no heads it will fallback to any person. +/// +[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 index 0000000000..4188b1da3d --- /dev/null +++ b/Content.Server/Objectives/Components/PickRandomPersonComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Sets the target for to a random person. +/// +[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 index 0000000000..fd37d0d2c8 --- /dev/null +++ b/Content.Server/Objectives/Components/RandomTraitorAliveComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Sets the target for to a random traitor. +/// +[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 index 0000000000..c05ac0d3ef --- /dev/null +++ b/Content.Server/Objectives/Components/RandomTraitorProgressComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Sets the target for to a random traitor. +/// +[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 index 0000000000..86f8d7cedf --- /dev/null +++ b/Content.Server/Objectives/Components/RoleRequirementComponent.cs @@ -0,0 +1,15 @@ +using Content.Server.Objectives.Systems; +using Content.Shared.Whitelist; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that the player's mind matches a whitelist. +/// Typical use is checking for (antagonist) roles. +/// +[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 index 0000000000..cd8b427848 --- /dev/null +++ b/Content.Server/Objectives/Components/SocialObjectiveComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Server.Objectives.Components; + +/// +/// Marker component for social objectives and kill objectives to be mutually exclusive. +/// +[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 index 0000000000..4fbe8572cd --- /dev/null +++ b/Content.Server/Objectives/Components/SpiderChargeConditionComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that the player is a ninja and blew up their spider charge at its target location. +/// +[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 index 0000000000..e148d772f4 --- /dev/null +++ b/Content.Server/Objectives/Components/SpiderChargeTargetRequirementComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires the player to be a ninja that has a spider charge target assigned, which is almost always the case. +/// +[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 index 0000000000..b52ac9cbe8 --- /dev/null +++ b/Content.Server/Objectives/Components/StealConditionComponent.cs @@ -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; + +/// +/// Requires that you steal a certain item. +/// +[RegisterComponent, Access(typeof(StealConditionSystem))] +public sealed partial class StealConditionComponent : Component +{ + /// + /// The id of the item to steal. + /// + /// + /// Works by prototype id not tags or anything so it has to be the exact item. + /// + [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + public string Prototype = string.Empty; + + /// + /// Help newer players by saying e.g. "steal the chief engineer's advanced magboots" + /// instead of "steal advanced magboots. Should be a loc string. + /// + [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 index 0000000000..736a2e74b6 --- /dev/null +++ b/Content.Server/Objectives/Components/StealResearchConditionComponent.cs @@ -0,0 +1,12 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Objective condition that requires the player to be a ninja and have stolen at least a random number of technologies. +/// Requires to function. +/// +[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 index 0000000000..80c1cc8eb0 --- /dev/null +++ b/Content.Server/Objectives/Components/SurviveConditionComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Just requires that the player is not dead, ignores evac and what not. +/// +[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 index 0000000000..c0cd521bc7 --- /dev/null +++ b/Content.Server/Objectives/Components/TargetObjectiveComponent.cs @@ -0,0 +1,21 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +[RegisterComponent, Access(typeof(TargetObjectiveSystem))] +public sealed partial class TargetObjectiveComponent : Component +{ + /// + /// Locale id for the objective title. + /// It is passed "targetName" and "job" arguments. + /// + [DataField(required: true), ViewVariables(VVAccess.ReadWrite)] + public string Title = string.Empty; + + /// + /// Mind entity id of the target. + /// This must be set by another system using . + /// + [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 index 0000000000..c94e3b424d --- /dev/null +++ b/Content.Server/Objectives/Components/TerrorConditionComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that the player is a ninja and has called in a threat. +/// +[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 index d5e9e1aef6..0000000000 --- a/Content.Server/Objectives/Conditions/DieCondition.cs +++ /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(); - var mindSystem = entityManager.System(); - 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 index 0752048554..0000000000 --- a/Content.Server/Objectives/Conditions/DoorjackCondition.cs +++ /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; - -/// -/// Objective condition that requires the player to be a ninja and have doorjacked at least a random number of airlocks. -/// -[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().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(); - if (!entMan.TryGetComponent(_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 index 27d7975c24..0000000000 --- a/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs +++ /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(); - var mindSystem = entMan.System(); - - if (_mind?.OwnedEntity == null - || !entMan.TryGetComponent(_mind.OwnedEntity, out var xform)) - return 0f; - - if (mindSystem.IsCharacterDeadIc(_mind)) - return 0f; - - if (entMan.TryGetComponent(_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(); - 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 index b5f16a62a2..0000000000 --- a/Content.Server/Objectives/Conditions/KillPersonCondition.cs +++ /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(); - protected SharedMindSystem Minds => EntityManager.System(); - protected SharedJobSystem Jobs => EntityManager.System(); - protected MobStateSystem MobStateSystem => EntityManager.System(); - protected EntityUid? TargetMindId; - protected MindComponent? TargetMind => EntityManager.GetComponentOrNull(TargetMindId); - public abstract IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind); - - /// - /// Whether the target must be truly dead, ignores missing evac. - /// - 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(); - var mindSystem = entMan.System(); - 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(); - if (!configMan.GetCVar(CCVars.EmergencyShuttleEnabled)) - return 0f; - - // target is escaping so you fail - var emergencyShuttle = entMan.System(); - 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 index 2703a37bc1..0000000000 --- a/Content.Server/Objectives/Conditions/KillRandomHeadCondition.cs +++ /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(true).Where(mc => - { - var entity = EntityManagerExt.GetComponentOrNull(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().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 index 74eb6422b2..0000000000 --- a/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs +++ /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(); - var query = EntityManager.EntityQuery(true); - foreach (var (mc, _) in query) - { - var entity = EntityManager.GetComponentOrNull(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().Pick(allHumans)}; - } -} diff --git a/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs b/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs deleted file mode 100644 index 8f914f0896..0000000000 --- a/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs +++ /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(); - - var traitors = Enumerable.ToList<(EntityUid Id, MindComponent Mind)>(entityMgr.System().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().Pick(traitors).Id }; - } - - public string Title - { - get - { - var targetName = string.Empty; - var ents = IoCManager.Resolve(); - var jobs = ents.System(); - 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(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(); - var mindSystem = entityManager.System(); - 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 index 4b0dc018aa..0000000000 --- a/Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs +++ /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(); - - var traitors = entityMgr.System().GetOtherTraitorMindsAliveAndConnected(mind).ToList(); - List 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().Pick(traitors).Id }; - } - - public string Title - { - get - { - var targetName = string.Empty; - var entities = IoCManager.Resolve(); - var jobs = entities.System(); - 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(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(); - 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 index 5209296842..0000000000 --- a/Content.Server/Objectives/Conditions/SpiderChargeCondition.cs +++ /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; - -/// -/// Objective condition that requires the player to be a ninja and have detonated their spider charge. -/// -[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(); - if (!entMan.TryGetComponent(_mind, out var role) - || role.SpiderChargeTarget == null - || !entMan.TryGetComponent(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(); - if (!entMan.TryGetComponent(_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 index 5ca36a7784..0000000000 --- a/Content.Server/Objectives/Conditions/StealCondition.cs +++ /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; - - /// - /// Help newer players by saying e.g. "steal the chief engineer's advanced magboots" - /// instead of "steal advanced magboots. Should be a loc string. - /// - [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().TryIndex(_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(); - - // TODO make this a container system function - // or: just iterate through transform children, instead of containers? - - var metaQuery = entMan.GetEntityQuery(); - var managerQuery = entMan.GetEntityQuery(); - var stack = new Stack(); - - 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 index 4c32f3c3fc..0000000000 --- a/Content.Server/Objectives/Conditions/StealResearchCondition.cs +++ /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; - -/// -/// Objective condition that requires the player to be a ninja and have stolen at least a random number of technologies. -/// -[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().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(); - if (!entMan.TryGetComponent(_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 index 98b5aa6c89..0000000000 --- a/Content.Server/Objectives/Conditions/SurviveCondition.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Content.Shared.Mind; -using Content.Shared.Objectives.Interfaces; -using Robust.Shared.Utility; - -namespace Content.Server.Objectives.Conditions; - -/// -/// Just requires that the player is not dead, ignores evac and what not. -/// -[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(); - if (!entMan.TryGetComponent(_mind, out var mind)) - return 0f; - - var mindSystem = entMan.System(); - 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 index 28cce20aa2..0000000000 --- a/Content.Server/Objectives/Conditions/TerrorCondition.cs +++ /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; - -/// -/// Objective condition that requires the player to be a ninja and have called in a threat. -/// -[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(); - if (!entMan.TryGetComponent(_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; - } -} diff --git a/Content.Server/Objectives/ObjectivesSystem.cs b/Content.Server/Objectives/ObjectivesSystem.cs index 86cc58438a..3f7e92963a 100644 --- a/Content.Server/Objectives/ObjectivesSystem.cs +++ b/Content.Server/Objectives/ObjectivesSystem.cs @@ -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 /// private void OnRoundEndText(RoundEndTextAppendEvent ev) { + // go through each gamerule getting data for the roundend summary. + var summaries = new Dictionary>>(); var query = EntityQueryEnumerator(); 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>(); + + 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; + // same prepended text (usually empty) so combine them + summary[prepend.Text].AddRange(info.Minds); + } + else + { + summary[prepend.Text] = info.Minds; + } + } - var name = mind.CharacterName; - _mind.TryGetSession(mindId, out var session); - var username = session?.Name; + // 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; + } - 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; - - 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)); - continue; - } - - result += Loc.GetString("objectives-with-objectives", ("title", title), ("agent", agent)); - - foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer)) - { - result += "\n" + Loc.GetString($"objective-issuer-{objectiveGroup.Key}"); - - foreach (var objective in objectiveGroup) - { - 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") - ); - } - } - } - } + AddSummary(ref result, agent, minds); } ev.AddLine(result + "\n"); } } - public ObjectivePrototype? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto) + private void AddSummary(ref string result, string agent, List 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; + + title = Loc.GetString("objectives-player-named", ("name", name)); + } + + 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(o).Issuer)) + { + result += "\n" + Loc.GetString($"objective-issuer-{objectiveGroup.Key}"); + + foreach (var objective in objectiveGroup) + { + var info = GetInfo(objective, mindId, mind); + if (info == null) + continue; + + var objectiveTitle = info.Value.Title; + var progress = info.Value.Progress; + if (progress > 0.99f) + { + 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") + ); + } + } + } + } + } + + public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto) { if (!_prototypeManager.TryIndex(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(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(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 index 9a235e53ad..0000000000 --- a/Content.Server/Objectives/Requirements/IncompatibleConditionsRequirement.cs +++ /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 _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 index 57209dcdbf..0000000000 --- a/Content.Server/Objectives/Requirements/IncompatibleObjectivesRequirement.cs +++ /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 _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 index 826894104c..0000000000 --- a/Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs +++ /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().GetOtherTraitorMindsAliveAndConnected(mind).Count >= _requiredTraitors; - } - } -} diff --git a/Content.Server/Objectives/Requirements/NinjaRequirement.cs b/Content.Server/Objectives/Requirements/NinjaRequirement.cs deleted file mode 100644 index 8a0993bed8..0000000000 --- a/Content.Server/Objectives/Requirements/NinjaRequirement.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.Roles; -using Content.Shared.Mind; -using Content.Shared.Objectives.Interfaces; - -namespace Content.Server.Objectives.Requirements; - -/// -/// Requires the player's mind to have the ninja role component, aka be a ninja. -/// -[DataDefinition] -public sealed partial class NinjaRequirement : IObjectiveRequirement -{ - public bool CanBeAssigned(EntityUid mindId, MindComponent mind) - { - var entMan = IoCManager.Resolve(); - return entMan.HasComponent(mindId); - } -} diff --git a/Content.Server/Objectives/Requirements/NotRoleRequirement.cs b/Content.Server/Objectives/Requirements/NotRoleRequirement.cs deleted file mode 100644 index f0b06f3b82..0000000000 --- a/Content.Server/Objectives/Requirements/NotRoleRequirement.cs +++ /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), required:true)] - private string _roleId = default!; - - /// - /// This requirement is met if the traitor is NOT the roleId, and fails if they are. - /// - public bool CanBeAssigned(EntityUid mindId, MindComponent mind) - { - // TODO ECS this shit i keep seeing shitcode everywhere - var entities = IoCManager.Resolve(); - 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 index 6bb6bbb7a8..0000000000 --- a/Content.Server/Objectives/Requirements/SpiderChargeTargetRequirement.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Server.Roles; -using Content.Shared.Mind; -using Content.Shared.Objectives.Interfaces; - -namespace Content.Server.Objectives.Requirements; - -/// -/// Requires the player to be a ninja that has a spider charge target assigned, which is almost always the case. -/// -[DataDefinition] -public sealed partial class SpiderChargeTargetRequirement : IObjectiveRequirement -{ - public bool CanBeAssigned(EntityUid mindId, MindComponent mind) - { - var entMan = IoCManager.Resolve(); - entMan.TryGetComponent(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 index e2bab8f471..0000000000 --- a/Content.Server/Objectives/Requirements/TraitorRequirement.cs +++ /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().System(); - return roleSystem.MindHasRole(mindId); - } - } -} diff --git a/Content.Server/Objectives/Systems/DieConditionSystem.cs b/Content.Server/Objectives/Systems/DieConditionSystem.cs new file mode 100644 index 0000000000..8bc384646d --- /dev/null +++ b/Content.Server/Objectives/Systems/DieConditionSystem.cs @@ -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(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 index 0000000000..5f8f680d9e --- /dev/null +++ b/Content.Server/Objectives/Systems/EscapeShuttleConditionSystem.cs @@ -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(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(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 index 0000000000..e4455c0381 --- /dev/null +++ b/Content.Server/Objectives/Systems/HelpProgressConditionSystem.cs @@ -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; + +/// +/// Handles help progress condition logic and picking random help targets. +/// +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(OnGetProgress); + + SubscribeLocalEvent(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(uid, out var target)) + { + args.Cancelled = true; + return; + } + + var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind) + .Select(pair => pair.Item1) + .ToHashSet(); + var removeList = new List(); + + // 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(traitor) or something when objectives are moved out of mind + if (!TryComp(traitor, out var mind)) + continue; + + foreach (var objective in mind.AllObjectives) + { + if (HasComp(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(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 index 0000000000..48df96e742 --- /dev/null +++ b/Content.Server/Objectives/Systems/KeepAliveCondition.cs @@ -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; + +/// +/// Handles keep alive condition logic and picking random traitors to keep alive. +/// +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(OnGetProgress); + + SubscribeLocalEvent(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(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(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 index 0000000000..c1caa819e4 --- /dev/null +++ b/Content.Server/Objectives/Systems/KillPersonConditionSystem.cs @@ -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; + +/// +/// Handles kill person condition logic and picking random kill targets. +/// +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(OnGetProgress); + + SubscribeLocalEvent(OnPersonAssigned); + + SubscribeLocalEvent(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(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(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(); + 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(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 index 0000000000..75811194fb --- /dev/null +++ b/Content.Server/Objectives/Systems/MultipleTraitorsRequirementSystem.cs @@ -0,0 +1,29 @@ +using Content.Server.GameTicking.Rules; +using Content.Server.Objectives.Components; +using Content.Shared.Objectives.Components; + +namespace Content.Server.Objectives.Systems; + +/// +/// Handles requiring multiple traitors being alive for the objective to be given. +/// +public sealed class MultipleTraitorsRequirementSystem : EntitySystem +{ + [Dependency] private readonly TraitorRuleSystem _traitorRule = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(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 index 0000000000..8e03ef201d --- /dev/null +++ b/Content.Server/Objectives/Systems/NinjaConditionsSystem.cs @@ -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; + +/// +/// Handles the objective conditions that hard depend on ninja. +/// Survive is handled by since it works without being a ninja. +/// +public sealed class NinjaConditionsSystem : EntitySystem +{ + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly NumberObjectiveSystem _number = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnDoorjackGetProgress); + + SubscribeLocalEvent(OnSpiderChargeAfterAssign); + SubscribeLocalEvent(OnSpiderChargeGetProgress); + + SubscribeLocalEvent(OnStealResearchGetProgress); + + SubscribeLocalEvent(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(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(args.MindId, out var role) && role.SpiderChargeDetonated ? 1f : 0f; + } + + private string SpiderChargeTitle(EntityUid mindId) + { + if (!TryComp(mindId, out var role) || + role.SpiderChargeTarget == null || + !TryComp(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(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(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 index 0000000000..e63492bb5e --- /dev/null +++ b/Content.Server/Objectives/Systems/NotCommandRequirementSystem.cs @@ -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(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 index 0000000000..c9539fcbf1 --- /dev/null +++ b/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs @@ -0,0 +1,31 @@ +using Content.Server.Objectives.Components; +using Content.Shared.Objectives.Components; +using Content.Shared.Roles.Jobs; + +namespace Content.Server.Objectives.Systems; + +/// +/// Handles checking the job blacklist for this objective. +/// +public sealed class NotJobRequirementSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(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(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 index 0000000000..3263284d77 --- /dev/null +++ b/Content.Server/Objectives/Systems/NumberObjectiveSystem.cs @@ -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; + +/// +/// Provides API for other components, handles picking the count and setting the title and description. +/// +public sealed class NumberObjectiveSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAssigned); + SubscribeLocalEvent(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); + } + + /// + /// Gets the objective's target count. + /// + 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 index 0000000000..5318b2e454 --- /dev/null +++ b/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs @@ -0,0 +1,26 @@ +using Content.Server.Objectives.Components; +using Content.Shared.Objectives.Components; + +namespace Content.Server.Objectives.Systems; + +/// +/// Handles applying the objective component blacklist to the objective entity. +/// +public sealed class ObjectiveBlacklistRequirementSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(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 index 0000000000..97aee218f0 --- /dev/null +++ b/Content.Server/Objectives/Systems/RoleRequirementSystem.cs @@ -0,0 +1,28 @@ +using Content.Server.Objectives.Components; +using Content.Shared.Objectives.Components; + +namespace Content.Server.Objectives.Systems; + +/// +/// Handles role requirement for objectives that require a certain (probably antagonist) role(s). +/// +public sealed class RoleRequirementSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(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 index 0000000000..107d88900a --- /dev/null +++ b/Content.Server/Objectives/Systems/SpiderChargeTargetRequirementSystem.cs @@ -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(OnCheck); + } + + private void OnCheck(EntityUid uid, SpiderChargeTargetRequirementComponent comp, ref RequirementCheckEvent args) + { + if (args.Cancelled) + return; + + if (!TryComp(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 index 0000000000..28ab164e0d --- /dev/null +++ b/Content.Server/Objectives/Systems/StealConditionSystem.cs @@ -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 containerQuery; + private EntityQuery metaQuery; + + public override void Initialize() + { + base.Initialize(); + + containerQuery = GetEntityQuery(); + metaQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnAssigned); + SubscribeLocalEvent(OnAfterAssign); + SubscribeLocalEvent(OnGetProgress); + } + + private void OnAssigned(EntityUid uid, StealConditionComponent comp, ref ObjectiveAssignedEvent args) + { + // cancel if the item to steal doesn't exist + args.Cancelled |= !_proto.HasIndex(comp.Prototype); + } + + private void OnAfterAssign(EntityUid uid, StealConditionComponent comp, ref ObjectiveAfterAssignEvent args) + { + var proto = _proto.Index(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(); + 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 index 0000000000..9bb7234301 --- /dev/null +++ b/Content.Server/Objectives/Systems/SurviveConditionSystem.cs @@ -0,0 +1,25 @@ +using Content.Server.Objectives.Components; +using Content.Shared.Objectives.Components; +using Content.Shared.Mind; + +namespace Content.Server.Objectives.Systems; + +/// +/// Handles progress for the survive objective condition. +/// +public sealed class SurviveConditionSystem : EntitySystem +{ + [Dependency] private readonly SharedMindSystem _mind = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(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 index 0000000000..82ebd28ca9 --- /dev/null +++ b/Content.Server/Objectives/Systems/TargetObjectiveSystem.cs @@ -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; + +/// +/// Provides API for other components and handles setting the title. +/// +public sealed class TargetObjectiveSystem : EntitySystem +{ + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedJobSystem _job = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(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); + } + + /// + /// Sets the Target field for the title and other components to use. + /// + public void SetTarget(EntityUid uid, EntityUid target, TargetObjectiveComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return; + + comp.Target = target; + } + + /// + /// Gets the target from the component. + /// + /// + /// If it is null then the prototype is invalid, just return. + /// + 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(target, out var mind) && mind.CharacterName != null) + { + targetName = mind.CharacterName; + } + + var jobName = _job.MindTryGetJobName(target); + return Loc.GetString(title, ("targetName", targetName), ("job", jobName)); + } + +} diff --git a/Content.Shared/CharacterInfo/SharedCharacterInfoSystem.cs b/Content.Shared/CharacterInfo/SharedCharacterInfoSystem.cs index b330af2629..550a81313a 100644 --- a/Content.Shared/CharacterInfo/SharedCharacterInfoSystem.cs +++ b/Content.Shared/CharacterInfo/SharedCharacterInfoSystem.cs @@ -19,10 +19,10 @@ public sealed class CharacterInfoEvent : EntityEventArgs { public readonly NetEntity NetEntity; public readonly string JobTitle; - public readonly Dictionary> Objectives; + public readonly Dictionary> Objectives; public readonly string? Briefing; - public CharacterInfoEvent(NetEntity netEntity, string jobTitle, Dictionary> objectives, string? briefing) + public CharacterInfoEvent(NetEntity netEntity, string jobTitle, Dictionary> objectives, string? briefing) { NetEntity = netEntity; JobTitle = jobTitle; diff --git a/Content.Shared/Mind/MindComponent.cs b/Content.Shared/Mind/MindComponent.cs index f195220e14..d6e30130e7 100644 --- a/Content.Shared/Mind/MindComponent.cs +++ b/Content.Shared/Mind/MindComponent.cs @@ -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 Objectives = new(); + internal readonly List Objectives = new(); /// /// The session ID of the player owning this mind. @@ -78,10 +77,10 @@ namespace Content.Shared.Mind // TODO move objectives out of mind component /// - /// An enumerable over all the objectives this mind has. + /// An enumerable over all the objective entities this mind has. /// [ViewVariables] - public IEnumerable AllObjectives => Objectives; + public IEnumerable AllObjectives => Objectives; /// /// Prevents user from ghosting out diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs index fc6cb8d570..91f68b0245 100644 --- a/Content.Shared/Mind/SharedMindSystem.cs +++ b/Content.Shared/Mind/SharedMindSystem.cs @@ -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 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(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 } /// - /// Adds an objective to this mind. + /// Tries to create and add an objective from its prototype id. /// - public bool TryAddObjective(EntityUid mindId, MindComponent mind, ObjectivePrototype objectivePrototype) + /// Returns true if adding the objective succeeded. + public bool TryAddObjective(EntityUid mindId, MindComponent mind, string proto) { - if (!objectivePrototype.CanBeAssigned(mindId, mind)) - return false; - var objective = objectivePrototype.GetObjective(mindId, mind); - if (mind.Objectives.Contains(objective)) + var objective = _objectives.TryCreateObjective(mindId, mind, proto); + if (objective == null) 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; } /// - /// Adds an objective, by id, to this mind. + /// Adds an objective that already exists, and is assumed to have had its requirements checked. /// - 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(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); } /// - /// Removes an objective to this mind. + /// Removes an objective from this mind. /// /// Returns true if the removal succeeded. - 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); } /// @@ -432,6 +419,30 @@ public abstract class SharedMindSystem : EntitySystem { return TryGetMind(userId, out _, out var mind) ? mind.CharacterName : null; } + + /// + /// Returns a list of every living humanoid player's minds, except for a single one which is exluded. + /// + public List GetAliveHumansExcept(EntityUid exclude) + { + var mindQuery = EntityQuery(); + + var allHumans = new List(); + // HumanoidAppearanceComponent is used to prevent mice, pAIs, etc from being chosen + var query = EntityQueryEnumerator(); + 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; + } } /// diff --git a/Content.Shared/Objectives/Components/ObjectiveComponent.cs b/Content.Shared/Objectives/Components/ObjectiveComponent.cs new file mode 100644 index 0000000000..95fbc68561 --- /dev/null +++ b/Content.Shared/Objectives/Components/ObjectiveComponent.cs @@ -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; + +/// +/// Required component for an objective entity prototype. +/// +[RegisterComponent, Access(typeof(SharedObjectivesSystem))] +public sealed partial class ObjectiveComponent : Component +{ + /// + /// Difficulty rating used to avoid assigning too many difficult objectives. + /// + [DataField(required: true), ViewVariables(VVAccess.ReadWrite)] + public float Difficulty; + + /// + /// Organisation that issued this objective, used for grouping and as a header above common objectives. + /// + [DataField(required: true), ViewVariables(VVAccess.ReadWrite)] + public string Issuer = string.Empty; + + /// + /// Unique objectives can only have 1 per prototype id. + /// Set this to false if you want multiple objectives of the same prototype. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool Unique = true; + + /// + /// Icon of this objective to display in the character menu. + /// Can be specified by an handler but is usually done in the prototype. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public SpriteSpecifier? Icon; +} + +/// +/// 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. +/// +[ByRefEvent] +public record struct RequirementCheckEvent(EntityUid MindId, MindComponent Mind, bool Cancelled = false); + +/// +/// Event raised on an objective after its requirements have been checked. +/// If is set to true, the objective is deleted. +/// Use this if the objective cannot be used, like a kill objective with no people alive. +/// +[ByRefEvent] +public record struct ObjectiveAssignedEvent(EntityUid MindId, MindComponent Mind, bool Cancelled = false); + +/// +/// Event raised on an objective after everything has handled . +/// Use this to set the objective's title description or icon. +/// +[ByRefEvent] +public record struct ObjectiveAfterAssignEvent(EntityUid MindId, MindComponent Mind, ObjectiveComponent Objective, MetaDataComponent Meta); + +/// +/// Event raised on an objective to update the Progress field. +/// To use this yourself call with the mind. +/// +[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 index 3aa335c8f8..0000000000 --- a/Content.Shared/Objectives/ConditionInfo.cs +++ /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 index 79e77e1759..0000000000 --- a/Content.Shared/Objectives/Interfaces/IObjectiveCondition.cs +++ /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 - { - /// - /// Returns a copy of the IObjectiveCondition which is assigned to the mind. - /// - /// Mind id to assign to. - /// Mind to assign to. - /// The new IObjectiveCondition. - IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind); - - /// - /// Returns the title of the condition. - /// - string Title { get; } - - /// - /// Returns the description of the condition. - /// - string Description { get; } - - /// - /// Returns a SpriteSpecifier to be used as an icon for the condition. - /// - SpriteSpecifier Icon { get; } - - /// - /// Returns the current progress of the condition in % from 0 to 1. - /// - /// Current progress in %. - float Progress { get; } - - /// - /// Returns a difficulty of the condition. - /// - float Difficulty { get; } - } -} diff --git a/Content.Shared/Objectives/Interfaces/IObjectiveRequirement.cs b/Content.Shared/Objectives/Interfaces/IObjectiveRequirement.cs deleted file mode 100644 index 973a0ea7b0..0000000000 --- a/Content.Shared/Objectives/Interfaces/IObjectiveRequirement.cs +++ /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 - { - /// - /// Checks whether or not the entity & its surroundings are valid to be given the objective. - /// - /// Returns true if objective can be given. - bool CanBeAssigned(EntityUid mindId, MindComponent mind); - } -} diff --git a/Content.Shared/Objectives/Objective.cs b/Content.Shared/Objectives/Objective.cs deleted file mode 100644 index 3f7b75281c..0000000000 --- a/Content.Shared/Objectives/Objective.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Content.Shared.Mind; -using Content.Shared.Objectives.Interfaces; - -namespace Content.Shared.Objectives -{ - public sealed class Objective : IEquatable - { - [ViewVariables] - public readonly EntityUid MindId; - [ViewVariables] - public readonly MindComponent Mind; - [ViewVariables] - public readonly ObjectivePrototype Prototype; - private readonly List _conditions = new(); - [ViewVariables] - public IReadOnlyList 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 index 0000000000..689fe17e6c --- /dev/null +++ b/Content.Shared/Objectives/ObjectiveInfo.cs @@ -0,0 +1,17 @@ +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Objectives; + +/// +/// 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. +/// +/// +/// 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 . +/// If anything is null it will be logged and return null. +/// +[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 index 286afb550a..0000000000 --- a/Content.Shared/Objectives/ObjectivePrototype.cs +++ /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 -{ - /// - /// 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 - /// - [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 _conditions = new(); - [DataField("requirements")] - private List _requirements = new(); - - [ViewVariables] - public IReadOnlyList 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 index 0000000000..dffb4e75c0 --- /dev/null +++ b/Content.Shared/Objectives/Systems/SharedObjectivesSystem.cs @@ -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; + +/// +/// Provides API for creating and interacting with objectives. +/// +public abstract class SharedObjectivesSystem : EntitySystem +{ + [Dependency] private readonly SharedMindSystem _mind = default!; + + private EntityQuery _metaQuery; + + public override void Initialize() + { + base.Initialize(); + + _metaQuery = GetEntityQuery(); + } + + /// + /// Checks requirements and duplicate objectives to see if an objective can be assigned. + /// + 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; + } + + /// + /// 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. + /// + public EntityUid? TryCreateObjective(EntityUid mindId, MindComponent mind, string proto) + { + var uid = Spawn(proto); + if (!TryComp(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; + } + + /// + /// Get the title, description, icon and progress of an objective using . + /// If any of them are null it is logged and null is returned. + /// + /// ID of the condition entity + /// ID of the player's mind entity + /// Mind component of the player's mind + 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(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); + } + + /// + /// Sets the objective's icon to the one specified. + /// Intended for handlers to set an icon. + /// + 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 index d00c3cca35..0000000000 --- a/Resources/Locale/en-US/objectives/conditions/die-condition.ftl +++ /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 diff --git a/Resources/Locale/en-US/objectives/conditions/doorjack-condition.ftl b/Resources/Locale/en-US/objectives/conditions/doorjack.ftl 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 e2bed1cef8..6e05fd279d 100644 --- a/Resources/Locale/en-US/objectives/conditions/doorjack-condition.ftl +++ b/Resources/Locale/en-US/objectives/conditions/doorjack.ftl @@ -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 index 5f950444aa..0000000000 --- a/Resources/Locale/en-US/objectives/conditions/escape-shuttle-condition.ftl +++ /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 index ad8861ba06..0000000000 --- a/Resources/Locale/en-US/objectives/conditions/kill-head-condition.ftl +++ /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 index 0000000000..dce2a94121 --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/kill-head.ftl @@ -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 index 8d25f44606..0000000000 --- a/Resources/Locale/en-US/objectives/conditions/kill-person-condition.ftl +++ /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 index 0000000000..c48e2122ff --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/kill-person.ftl @@ -0,0 +1 @@ +objective-condition-kill-person-title = Kill or maroon {$targetName}, {CAPITALIZE($job)} diff --git a/Resources/Locale/en-US/objectives/conditions/other-traitor-alive-condition.ftl b/Resources/Locale/en-US/objectives/conditions/other-traitor-alive.ftl 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 54ddc31c10..524d0dde63 100644 --- a/Resources/Locale/en-US/objectives/conditions/other-traitor-alive-condition.ftl +++ b/Resources/Locale/en-US/objectives/conditions/other-traitor-alive.ftl @@ -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. diff --git a/Resources/Locale/en-US/objectives/conditions/other-traitor-progress-condition.ftl b/Resources/Locale/en-US/objectives/conditions/other-traitor-progress.ftl 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 883e61ce4f..4ee832d7ce 100644 --- a/Resources/Locale/en-US/objectives/conditions/other-traitor-progress-condition.ftl +++ b/Resources/Locale/en-US/objectives/conditions/other-traitor-progress.ftl @@ -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 index 3ce7a983b2..0000000000 --- a/Resources/Locale/en-US/objectives/conditions/spider-charge-condition.ftl +++ /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 index 0000000000..cdc3cfda96 --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/spider-charge.ftl @@ -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 index a9a820c3c5..0000000000 --- a/Resources/Locale/en-US/objectives/conditions/steal-research-condition.ftl +++ /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 index 0000000000..fe5b74660a --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/steal-research.ftl @@ -0,0 +1 @@ +objective-condition-steal-research-title = Steal {$count} technologies. diff --git a/Resources/Locale/en-US/objectives/conditions/steal-condition.ftl b/Resources/Locale/en-US/objectives/conditions/steal.ftl similarity index 100% rename from Resources/Locale/en-US/objectives/conditions/steal-condition.ftl rename to Resources/Locale/en-US/objectives/conditions/steal.ftl 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 index 5c9115a79f..0000000000 --- a/Resources/Locale/en-US/objectives/conditions/survive-condition.ftl +++ /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 index 104f5782dd..0000000000 --- a/Resources/Locale/en-US/objectives/conditions/terror-condition.ftl +++ /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. diff --git a/Resources/Locale/en-US/objectives/round-end.ftl b/Resources/Locale/en-US/objectives/round-end.ftl index c5ef2aacff..4c0e5884ca 100644 --- a/Resources/Locale/en-US/objectives/round-end.ftl +++ b/Resources/Locale/en-US/objectives/round-end.ftl @@ -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}%) diff --git a/Resources/Prototypes/GameRules/midround.yml b/Resources/Prototypes/GameRules/midround.yml index 0d5983b178..6d431a9dcd 100644 --- a/Resources/Prototypes/GameRules/midround.yml +++ b/Resources/Prototypes/GameRules/midround.yml @@ -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 index 0000000000..b4ce16a6f9 --- /dev/null +++ b/Resources/Prototypes/Objectives/base_objectives.yml @@ -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 index 0000000000..a7291645c9 --- /dev/null +++ b/Resources/Prototypes/Objectives/ninja.yml @@ -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 index f3df853d28..0000000000 --- a/Resources/Prototypes/Objectives/ninjaObjectives.yml +++ /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 {} diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index 1c4034a854..01f3dd4109 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -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 index 0000000000..9ef34b8cab --- /dev/null +++ b/Resources/Prototypes/Objectives/traitor.yml @@ -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 index a47d30f143..0000000000 --- a/Resources/Prototypes/Objectives/traitorObjectives.yml +++ /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