From 3df223751828b0a754c643e275e8a8ca29413d55 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:05:21 -0500 Subject: [PATCH 01/54] add IPlayerRequirement and requirement context --- .../PlayerRequirementManager.Context.cs | 22 ++++++ .../PlayerRequirements/IPlayerRequirement.cs | 67 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs create mode 100644 Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs new file mode 100644 index 0000000000..1737ea121f --- /dev/null +++ b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs @@ -0,0 +1,22 @@ +using Content.Shared.Players.PlayTimeTracking; +using Content.Shared.Preferences; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Requirements.Managers; + +/// +/// A record that represents a list of factors that will be checked against PlayerRequirements. +/// All fields are nullable. A null field represents an optional parameter. +/// +public partial record PlayerRequirementContext +{ + /// + /// The currently-selected character profile of the player we are going to check. + /// + public HumanoidCharacterProfile? Profile = null; + + /// + /// A dictionary of registered playtimes for the player. + /// + public Dictionary, TimeSpan>? Playtimes = null; +} diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs new file mode 100644 index 0000000000..14a99088db --- /dev/null +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs @@ -0,0 +1,67 @@ +using Content.Shared._DEN.Requirements.Managers; + +namespace Content.Shared._DEN.Requirements.PlayerRequirements; + +/// +/// This interface is used to check some parameters associated with a player (a +/// ) to determine whether or not the player is allowed to +/// use a certain feature. +/// +/// +/// This is a replacement of the old JobRequirement system. +/// +[ImplicitDataDefinitionForInheritors] +public partial interface IPlayerRequirement +{ + /// + /// Whether or not this requirement is inverted. + /// In all cases where this requirement should fail, it will succeed instead, and vice versa. + /// + [DataField] + bool Inverted { get; set; } + + /// + /// Requirements undergo a "PreCheck" to check if the context value has all relevant parameters. + /// If this is false, then the requirement auto-passes the requirement if the pre-check fails. + /// + /// + /// Say you have a requirement that checks your playtimes. In the pre-check, we check if the + /// context's playtimes are non-null. If your playtimes are null, we fail the pre-check. + /// + /// If MustPassPreCheck is false, then this will treat it as auto-passing the requirement; + /// we ignore the requirement entirely. If MustPassPreCheck is true, we auto-fail the + /// requirement instead. + /// + [DataField] + bool MustPassPreCheck { get; set; } + + /// + /// Check if the given context has all parameters required in order to perform an actual check. + /// + /// + /// Because context parameters are meant to be optional, we can use this information to ignore + /// (auto-succeed) checks where we lack all the needded parameters, depending on the value of + /// . + /// + /// A definition of parameters to check against the requirement. + /// + bool PreCheck(PlayerRequirementContext context); + + /// + /// Check a context's fields against this requirement to determine if it passes or fails. + /// + /// A definition of parameters to check against the requirement. + /// Whether or not the given context passes this requirement. + bool CheckRequirement(PlayerRequirementContext context); + + /// + /// Get a localized string representing how this requirement displays to players. + /// + /// + /// This should, ideally, display regardless if the context actually passes or not. + /// It should also display differently depending on the value of . + /// + /// A definition of parameters to check against the requirement. + /// An optional string representing the requirement text to display to a player. + string? GetReason(PlayerRequirementContext context); +} From d4c5754a6be68905721bfc11f558cab94e41d278 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:37:16 -0500 Subject: [PATCH 02/54] add player requirement manager --- .../Managers/PlayerRequirementManager.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs new file mode 100644 index 0000000000..6e01650a7e --- /dev/null +++ b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs @@ -0,0 +1,37 @@ +using Content.Shared._DEN.Requirements.PlayerRequirements; +using JetBrains.Annotations; + +namespace Content.Shared._DEN.Requirements.Managers; + +public sealed partial class PlayerRequirementManager +{ + /// + /// Check a context against enumerable requirements and gets the final pass/fail status of these requirements. + /// + /// The context containing fields to check against the requirements. + /// An enumerable collection of requirements. + /// Whether or not this context passes *all* requirements. If even one fails, then this is false. + [PublicAPI] + public static bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements) + { + foreach (var requirement in requirements) + { + // Pre-check the actual requirement. This ensures our context has all the fields needed for the requirement. + // If the pre-check fails, whether or not this requirement passes depends on requirement.MustPassPreCheck. + // If you don't need to pass the pre-check, then it's an auto-success. + if (!requirement.PreCheck(context)) + { + if (requirement.MustPassPreCheck) + return false; + + continue; + } + + // Check the actual requirement, now. + if (!requirement.CheckRequirement(context)) + return false; + } + + return true; + } +} From d2a932931b8c3d0b3e02a7c83efc327deac98c75 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:37:25 -0500 Subject: [PATCH 03/54] add department playtime requirements --- .../PlayerRequirements.Playtime.cs | 174 ++++++++++++++++++ .../requirements/playtime-requirements.ftl | 6 + 2 files changed, 180 insertions(+) create mode 100644 Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs create mode 100644 Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs new file mode 100644 index 0000000000..9ccb893f1d --- /dev/null +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -0,0 +1,174 @@ +using Content.Shared._DEN.Requirements.Managers; +using Content.Shared.Roles; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Requirements.PlayerRequirements; + +/// +/// An abstract class for playtime requirements that expect a playtime to be within +/// optional minimum and maximum parameters. +/// +public abstract partial class PlayerPlaytimeRequirement : IPlayerRequirement +{ + /// + [DataField] public bool Inverted { get; set; } = false; + + /// + [DataField] public bool MustPassPreCheck { get; set; } = false; + + /// + /// The minimum time you can have in this tracker. + /// + [DataField] public TimeSpan? MinTime = null; + + /// + /// The maximum time you can have in this tracker. + /// + [DataField] public TimeSpan? MaxTime = null; + + /// + public bool PreCheck(PlayerRequirementContext context) + { + return context.Playtimes != null; + } + + /// + public abstract bool CheckRequirement(PlayerRequirementContext context); + + /// + public abstract string? GetReason(PlayerRequirementContext context); + + /// + /// Check if a given playtime tracker fits within the minimum and maximum times of this requirement. + /// + /// The playtime to check. + /// Whether or not this playtime is valid. + protected bool IsValidPlaytime(TimeSpan playtime) + { + if (MinTime != null & playtime < MinTime) + return false; + + if (MaxTime != null & playtime > MaxTime) + return false; + + return true; + } + + /// + /// Gets a localized "reason" string for this requirement's playtime ranges. + /// + /// + /// For example: "Less than 30 minutes", "At least 120 minutes", "Between 20 minutes and 180 minutes". + /// + /// + /// A string describing how much playtime you should have. Null if both minimum and maximum as null. + /// + protected string? GetPlaytimeConstraintReason() + { + if (MinTime == null && MaxTime == null) + return null; + + var minTimeString = FormatPlaytime(MinTime); + var maxTimeString = FormatPlaytime(MaxTime); + + return (MinTime, MaxTime) switch + { + (not null, not null) => Loc.GetString("player-requirement-playtime-minmax-time", + ("minimum", minTimeString), ("maximum", maxTimeString)), + + (null, not null) => Loc.GetString("player-requirement-playtime-maximum-time", + ("playtime", maxTimeString)), + + (not null, null) => Loc.GetString("player-requirement-playtime-minimum-time", + ("playtime", minTimeString)), + + _ => null + }; + } + + /// + /// Gets a localized time string for the given playtime. + /// + /// The playtime to format. + /// A localized time string for this playtime. + private static string FormatPlaytime(TimeSpan? playtime) + { + var time = ((int?)playtime?.TotalMinutes) ?? 0; + + return playtime != null + ? Loc.GetString("player-requirement-playtime-time", ("playtime", time)) + : string.Empty; + } +} + +/// +/// Checks if a player's total playtime in a given department fits within a given playtime range. +/// +public sealed partial class PlayerDepartmentPlaytimeRequirement : PlayerPlaytimeRequirement +{ + /// + /// The department we should check against the requirement. + /// + [DataField(required: true)] + public ProtoId Department = default!; + + /// + public override bool CheckRequirement(PlayerRequirementContext context) + { + var playtime = GetDepartmentPlaytime(context); + if (playtime is null) + return false; + + return IsValidPlaytime(playtime.Value); + } + + /// + public override string? GetReason(PlayerRequirementContext context) + { + // Get the department name. + var protoMan = IoCManager.Resolve(); + if (!protoMan.TryIndex(Department, out var department)) + return null; + + var deptName = Loc.GetString(department.Name); + + // Get the playtime constraint string. + var playtimeString = GetPlaytimeConstraintReason(); + if (playtimeString == null) + return null; + + // E.g. "You must have 120 minutes in the Science department." + return Loc.GetString("player-requirement-department-playtime-reason", + ("timeConstraint", playtimeString), + ("department", deptName)); + } + + /// + /// Get the total playtime for this department. + /// + /// A definition of parameters to check against the requirement. + /// The total playtime of this department. Null if either context playtimes or department is invalid. + private TimeSpan? GetDepartmentPlaytime(PlayerRequirementContext context) + { + var protoMan = IoCManager.Resolve(); + var playtime = TimeSpan.Zero; + + if (context.Playtimes == null + || !protoMan.TryIndex(Department, out var department)) + return null; + + // Sum the playtimes of all roles in this department. + foreach (var roleId in department.Roles) + { + if (!protoMan.TryIndex(roleId, out var role)) + continue; + + if (!context.Playtimes.TryGetValue(role.PlayTimeTracker, out var roleTime)) + continue; + + playtime += roleTime; + } + + return playtime; + } +} diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl new file mode 100644 index 0000000000..4da5160306 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl @@ -0,0 +1,6 @@ +player-requirement-playtime-time = {$playtime} minutes +player-requirement-playtime-minimum-time = at least {$playtime} +player-requirement-playtime-maximum-time = less than {$playtime} +player-requirement-playtime-minmax-time = between {$minimum} and {$maximum} + +player-requirement-department-playtime-reason = Must have {$timeConstraint} in the {$department} department. From 3b5240aced798c8c03794ce9296a026680e5dd51 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:13:21 -0500 Subject: [PATCH 04/54] redundancy --- .../_DEN/Requirements/Managers/PlayerRequirementManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs index 6e01650a7e..8eafb3d728 100644 --- a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs +++ b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs @@ -16,7 +16,7 @@ public static bool CheckRequirements(PlayerRequirementContext context, IEnumerab { foreach (var requirement in requirements) { - // Pre-check the actual requirement. This ensures our context has all the fields needed for the requirement. + // Pre-check the requirement. This ensures our context has all the fields needed for the requirement. // If the pre-check fails, whether or not this requirement passes depends on requirement.MustPassPreCheck. // If you don't need to pass the pre-check, then it's an auto-success. if (!requirement.PreCheck(context)) From d6253d5c9ad9e1b0f12ccdb13f33dd6931b98cd3 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:18:47 -0500 Subject: [PATCH 05/54] add abstract player requirement class --- .../PlayerRequirements/IPlayerRequirement.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs index 14a99088db..2458beefec 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs @@ -65,3 +65,24 @@ public partial interface IPlayerRequirement /// An optional string representing the requirement text to display to a player. string? GetReason(PlayerRequirementContext context); } + +/// +/// Abstract class inherited by other player requirements. +/// +public abstract partial class PlayerRequirement : IPlayerRequirement +{ + /// + [DataField] public bool Inverted { get; set; } = false; + + /// + [DataField] public bool MustPassPreCheck { get; set; } = false; + + /// + public abstract bool CheckRequirement(PlayerRequirementContext context); + + /// + public abstract string? GetReason(PlayerRequirementContext context); + + /// + public abstract bool PreCheck(PlayerRequirementContext context); +} From 651a20f13bd433a50694a14266766952645c65d5 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:19:00 -0500 Subject: [PATCH 06/54] add inverted string to playtime reasons --- .../PlayerRequirements.Playtime.cs | 22 ++++++------------- .../requirements/playtime-requirements.ftl | 6 ++++- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs index 9ccb893f1d..b51b7cb060 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -8,14 +8,8 @@ namespace Content.Shared._DEN.Requirements.PlayerRequirements; /// An abstract class for playtime requirements that expect a playtime to be within /// optional minimum and maximum parameters. /// -public abstract partial class PlayerPlaytimeRequirement : IPlayerRequirement +public abstract partial class PlayerPlaytimeRequirement : PlayerRequirement { - /// - [DataField] public bool Inverted { get; set; } = false; - - /// - [DataField] public bool MustPassPreCheck { get; set; } = false; - /// /// The minimum time you can have in this tracker. /// @@ -27,17 +21,11 @@ public abstract partial class PlayerPlaytimeRequirement : IPlayerRequirement [DataField] public TimeSpan? MaxTime = null; /// - public bool PreCheck(PlayerRequirementContext context) + public override bool PreCheck(PlayerRequirementContext context) { return context.Playtimes != null; } - /// - public abstract bool CheckRequirement(PlayerRequirementContext context); - - /// - public abstract string? GetReason(PlayerRequirementContext context); - /// /// Check if a given playtime tracker fits within the minimum and maximum times of this requirement. /// @@ -137,9 +125,13 @@ public override bool CheckRequirement(PlayerRequirementContext context) if (playtimeString == null) return null; + var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason", + ("inverted", Inverted), + ("timeConstraint", playtimeString)); + // E.g. "You must have 120 minutes in the Science department." return Loc.GetString("player-requirement-department-playtime-reason", - ("timeConstraint", playtimeString), + ("constraint", constraintReason), ("department", deptName)); } diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl index 4da5160306..70f4ad6099 100644 --- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl @@ -2,5 +2,9 @@ player-requirement-playtime-time = {$playtime} minutes player-requirement-playtime-minimum-time = at least {$playtime} player-requirement-playtime-maximum-time = less than {$playtime} player-requirement-playtime-minmax-time = between {$minimum} and {$maximum} +player-requirement-playtime-constraint-reason = Must {$inverted -> + [true] not + *[false] {""} +} have {$timeConstraint} -player-requirement-department-playtime-reason = Must have {$timeConstraint} in the {$department} department. +player-requirement-department-playtime-reason = {$constraint} in the {$department} department. From 58876e30c4b20a6ad6830bc4ad30810d37749896 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:04:53 -0500 Subject: [PATCH 07/54] Add count requirements --- .../PlayerRequirements/CountRequirement.cs | 137 ++++++++++++++++++ .../_DEN/requirements/requirement-range.ftl | 5 + 2 files changed, 142 insertions(+) create mode 100644 Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs create mode 100644 Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs new file mode 100644 index 0000000000..9a3b3df26a --- /dev/null +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs @@ -0,0 +1,137 @@ +using System.Linq; +using JetBrains.Annotations; + +namespace Content.Shared._DEN.Requirements.PlayerRequirements; + +/// +/// An abstract class for requirements to determine how many items in a set should be selected. +/// +/// +/// For example: A player selecting multiple traits that overlap with a certain required group of traits. +/// +[ImplicitDataDefinitionForInheritors] +[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] +public abstract partial class CountRequirement +{ + /// + /// Gets a string reason representation for this range. + /// + /// + /// This would slot into the sentence "You must have [reason] of the following items: [items]". + /// + /// "At least 1", "between 2 and 5", "all" + /// A string representation of this range's requirement bounds. + public abstract string GetReason(); + + /// + /// Check if our currently-selected items in a collection meets this + /// requirement, against a collection of required items. + /// + /// The type of the collection. + /// The items we have selected. + /// The items required for this condition. + /// Whether or not we fulfill this requirement. + public abstract bool CheckRequirement(IEnumerable have, IEnumerable required); + + /// + /// Get how many of the required items in a collection we currently have. + /// + /// The type of the collection. + /// The items we have selected. + /// The items required for this condition. + /// How many of the required items we have. + protected static int GetFulfilledCount(IEnumerable have, IEnumerable required) + { + return have.Intersect(required).Count(); + } +} + +/// +/// To fulfill this requirement, you must have exactly some number of items. +/// +public sealed partial class ConstantCountRequirement : CountRequirement +{ + /// + /// How many items in the collection you need to pass the requirement. + /// + [DataField] + public int Count; + + /// + public override string GetReason() + { + return Loc.GetString("count-requirement-constant-reason", + ("count", Count)); + } + + /// + public override bool CheckRequirement(IEnumerable have, IEnumerable required) + { + var count = GetFulfilledCount(have, required); + return count == Count; + } +} + +/// +/// To fulfill this requirement, you must have a number of items between two values, or either a minimum / maximum. +/// +public sealed partial class RangeCountRequirement : CountRequirement +{ + /// + /// Minimum amount of required items you need. + /// + [DataField] + public int? Min = null; + + /// + /// Maximum amount of required items you can have. + /// + [DataField] + public int? Max = null; + + /// + public override string GetReason() + { + return (Min, Max) switch + { + (not null, not null) => Loc.GetString("count-requirement-range-minmax-reason", + ("minimum", Min), + ("maximum", Max)), + + (null, not null) => Loc.GetString("count-requirement-range-maximum-reason", + ("maximum", Max)), + + (not null, null) => Loc.GetString("count-requirement-range-minimum-reason", + ("minimum", Min)), + + _ => string.Empty + }; + } + + /// + public override bool CheckRequirement(IEnumerable have, IEnumerable required) + { + var count = GetFulfilledCount(have, required); + return (Min == null || count >= Min) + && (Max == null || count <= Max); + } +} + +/// +/// To fulfill the requirement, you must have all items in the required collection. +/// +public sealed partial class AllCountRequirement : CountRequirement +{ + /// + public override string GetReason() + { + return Loc.GetString("count-requirement-all-reason"); + } + + /// + public override bool CheckRequirement(IEnumerable have, IEnumerable required) + { + var count = GetFulfilledCount(have, required); + return count == required.Count(); + } +} diff --git a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl new file mode 100644 index 0000000000..b869b2b179 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl @@ -0,0 +1,5 @@ +count-requirement-constant-reason = exactly {$count} +count-requirement-range-minimum-reason = at least {$minimum} +count-requirement-range-maximum-reason = at most {$maximum} +count-requirement-range-minmax-reason = between {$minimum} and {$maximum} +count-requirement-all-reason = all From 2ff98abfd4767c0a3cb06350520970d2325a44b5 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:05:01 -0500 Subject: [PATCH 08/54] Awagga --- .../Locale/en-US/_DEN/requirements/playtime-requirements.ftl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl index 70f4ad6099..1b7a38a63b 100644 --- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl @@ -2,9 +2,9 @@ player-requirement-playtime-time = {$playtime} minutes player-requirement-playtime-minimum-time = at least {$playtime} player-requirement-playtime-maximum-time = less than {$playtime} player-requirement-playtime-minmax-time = between {$minimum} and {$maximum} -player-requirement-playtime-constraint-reason = Must {$inverted -> +player-requirement-playtime-constraint-reason = Must{$inverted -> [true] not - *[false] {""} + *[false] {" "} } have {$timeConstraint} player-requirement-department-playtime-reason = {$constraint} in the {$department} department. From b7afe20a9d962aa131c9e5975b6c9b81d5dadb83 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:05:51 -0500 Subject: [PATCH 09/54] add trait requirements --- .../PlayerRequirements.Profile.cs | 67 +++++++++++++++++++ .../requirements/profile-requirements.ftl | 5 ++ 2 files changed, 72 insertions(+) create mode 100644 Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs create mode 100644 Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs new file mode 100644 index 0000000000..129464701b --- /dev/null +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs @@ -0,0 +1,67 @@ +using System.Linq; +using Content.Shared._DEN.Requirements.Managers; +using Content.Shared._DEN.Traits.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Requirements.PlayerRequirements; + +/// +/// Checks if a player's character has a required number of the given traits. +/// +public sealed partial class PlayerTraitRequirement : PlayerRequirement +{ + /// + /// Traits that the character needs to have to the pass the requirement. + /// + [DataField] + public HashSet> Traits = new(); + + [DataField] + public CountRequirement Count; + + /// + public override bool PreCheck(PlayerRequirementContext context) + { + return context.Profile != null; + } + + /// + public override bool CheckRequirement(PlayerRequirementContext context) + { + if (context.Profile == null) + return false; + + var profileTraits = context.Profile.EntityTraitPreferences; + return Count.CheckRequirement(profileTraits, Traits); + } + + /// + public override string? GetReason(PlayerRequirementContext context) + { + var protoMan = IoCManager.Resolve(); + var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan)); + var traitList = string.Join(", ", traitNames); + var constraintReason = Count.GetReason(); + + return Loc.GetString("player-requirement-trait-reason", + ("inverted", Inverted), + ("constraint", constraintReason), + ("traits", traitList)); + } + + /// + /// Localizes a trait ID into a formatted trait name. + /// + /// The ID of the trait. + /// The prototype manager. + /// A formatted trait name string. + private static string LocalizeTrait(ProtoId traitId, IPrototypeManager protoMan) + { + var traitName = traitId; + + if (protoMan.TryIndex(traitId, out var trait)) + traitName = Loc.GetString(trait.Name); + + return Loc.GetString("player-requirement-trait", ("traitName", traitName)); + } +} diff --git a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl new file mode 100644 index 0000000000..79abfd63bb --- /dev/null +++ b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl @@ -0,0 +1,5 @@ +player-requirement-trait = [color=LightBlue]{$traitName}[/color] +player-requirement-trait-reason = Must{$inverted -> + [true] not + *[false] {" "} +} have {$constraint} of these traits: {$traits}; From 207c653e7c7973fb987546a29894bc1e9a56260d Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:15:30 -0500 Subject: [PATCH 10/54] add player requirements to IOC --- Content.Shared/IoC/SharedContentIoC.cs | 4 +++- .../_DEN/Requirements/Managers/PlayerRequirementManager.cs | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Content.Shared/IoC/SharedContentIoC.cs b/Content.Shared/IoC/SharedContentIoC.cs index f23b9f8355..fd69196de0 100644 --- a/Content.Shared/IoC/SharedContentIoC.cs +++ b/Content.Shared/IoC/SharedContentIoC.cs @@ -1,4 +1,5 @@ -using Content.Shared.Humanoid.Markings; +using Content.Shared._DEN.Requirements.Managers; +using Content.Shared.Humanoid.Markings; using Content.Shared.Localizations; namespace Content.Shared.IoC @@ -9,6 +10,7 @@ public static void Register(IDependencyCollection deps) { deps.Register(); deps.Register(); + deps.Register(); // DEN } } } diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs index 8eafb3d728..696b63ee07 100644 --- a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs +++ b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs @@ -1,8 +1,14 @@ using Content.Shared._DEN.Requirements.PlayerRequirements; using JetBrains.Annotations; +#pragma warning disable IDE1006 // Naming Styles namespace Content.Shared._DEN.Requirements.Managers; +#pragma warning restore IDE1006 // Naming Styles +/// +/// A manager used to check player stats against a list of requirements, getting the pass/fail status of these requirements. +/// This can be used to apply restrictions to character actions, like jobs or traits. +/// public sealed partial class PlayerRequirementManager { /// From fdd11f242f036eb09346c2c5e71bf80d42535655 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:48:58 -0500 Subject: [PATCH 11/54] comments --- .../_DEN/Requirements/PlayerRequirements/CountRequirement.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs index 9a3b3df26a..a22a7d40e9 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs @@ -60,6 +60,7 @@ public sealed partial class ConstantCountRequirement : CountRequirement /// public override string GetReason() { + // "You must have exactly 1 of the following items." return Loc.GetString("count-requirement-constant-reason", ("count", Count)); } @@ -94,13 +95,16 @@ public override string GetReason() { return (Min, Max) switch { + // "You must have between 1 and 5 of the following items." (not null, not null) => Loc.GetString("count-requirement-range-minmax-reason", ("minimum", Min), ("maximum", Max)), + // "You must have at most 5 of the following items." (null, not null) => Loc.GetString("count-requirement-range-maximum-reason", ("maximum", Max)), + // "You must have at least 1 of the following items." (not null, null) => Loc.GetString("count-requirement-range-minimum-reason", ("minimum", Min)), @@ -125,6 +129,7 @@ public sealed partial class AllCountRequirement : CountRequirement /// public override string GetReason() { + // "You must have all of the following items." return Loc.GetString("count-requirement-all-reason"); } From 6942b61c5f73b84719187877c40e8537b6f0aabc Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:05:17 -0500 Subject: [PATCH 12/54] add auto-pass to playtime requirements --- .../PlayerRequirements.Playtime.cs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs index b51b7cb060..b22bc123e3 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -1,5 +1,7 @@ using Content.Shared._DEN.Requirements.Managers; +using Content.Shared.CCVar; using Content.Shared.Roles; +using Robust.Shared.Configuration; using Robust.Shared.Prototypes; namespace Content.Shared._DEN.Requirements.PlayerRequirements; @@ -23,7 +25,10 @@ public abstract partial class PlayerPlaytimeRequirement : PlayerRequirement /// public override bool PreCheck(PlayerRequirementContext context) { - return context.Playtimes != null; + // We are always returning "true" if ShouldAutoPass() is true, because otherwise, if the + // pre-check failed, then it would be possible to fail this requirement as per + // PlayerRequirement.MustPassPreCheck even when role timers should be ignored anyway. + return ShouldAutoPass() || context.Playtimes != null; } /// @@ -42,6 +47,20 @@ protected bool IsValidPlaytime(TimeSpan playtime) return true; } + /// + /// Whether or not this requirement should auto-pass. This applies if role timers + /// are disabled, because playtimes shouldn't matter anyway in this case - we shouldn't + /// fail playtime requirements ever when role timers are disabled. + /// + /// + /// Whether or not this requirement should auto-pass. + /// + protected static bool ShouldAutoPass() + { + var config = IoCManager.Resolve(); + return !config.GetCVar(CCVars.GameRoleTimers); + } + /// /// Gets a localized "reason" string for this requirement's playtime ranges. /// @@ -53,6 +72,10 @@ protected bool IsValidPlaytime(TimeSpan playtime) /// protected string? GetPlaytimeConstraintReason() { + var config = IoCManager.Resolve(); + if (!config.GetCVar(CCVars.GameRoleTimers)) + return null; + if (MinTime == null && MaxTime == null) return null; @@ -103,6 +126,10 @@ public sealed partial class PlayerDepartmentPlaytimeRequirement : PlayerPlaytime /// public override bool CheckRequirement(PlayerRequirementContext context) { + // Auto-pass if role timers are disabled. + if (ShouldAutoPass()) + return true; + var playtime = GetDepartmentPlaytime(context); if (playtime is null) return false; @@ -113,6 +140,10 @@ public override bool CheckRequirement(PlayerRequirementContext context) /// public override string? GetReason(PlayerRequirementContext context) { + // Do not give a reason if role timers are disabled. + if (ShouldAutoPass()) + return null; + // Get the department name. var protoMan = IoCManager.Resolve(); if (!protoMan.TryIndex(Department, out var department)) From 5cd22c64b31a7e83c14e8eea23178dce8c4e302e Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:09:09 -0500 Subject: [PATCH 13/54] add IPlayerRequirementManager interface --- .../Managers/IPlayerRequirementManager.cs | 22 +++++++++++++++++++ ...Context.cs => PlayerRequirementContext.cs} | 0 ...r.cs => SharedPlayerRequirementManager.cs} | 17 ++++++-------- 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs rename Content.Shared/_DEN/Requirements/Managers/{PlayerRequirementManager.Context.cs => PlayerRequirementContext.cs} (100%) rename Content.Shared/_DEN/Requirements/Managers/{PlayerRequirementManager.cs => SharedPlayerRequirementManager.cs} (61%) diff --git a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs new file mode 100644 index 0000000000..03dd15e39d --- /dev/null +++ b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs @@ -0,0 +1,22 @@ +using Content.Shared._DEN.Requirements.PlayerRequirements; +using Robust.Shared.Player; + +namespace Content.Shared._DEN.Requirements.Managers; + +public interface IPlayerRequirementManager +{ + /// + /// Check a context against enumerable requirements and gets the final pass/fail status of these requirements. + /// + /// The context containing fields to check against the requirements. + /// An enumerable collection of requirements. + /// Whether or not this context passes *all* requirements. If even one fails, then this is false. + bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements); + + /// + /// Creates a new PlayerRequirementContext with context fields pre-filled. + /// + /// The session associated with this player. + /// A pre-filled requirement context for this player. + PlayerRequirementContext GetPlayerContext(ICommonSession session); +} diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs similarity index 100% rename from Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs rename to Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs similarity index 61% rename from Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs rename to Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs index 696b63ee07..4a317b521c 100644 --- a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs +++ b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs @@ -1,24 +1,18 @@ using Content.Shared._DEN.Requirements.PlayerRequirements; using JetBrains.Annotations; +using Robust.Shared.Player; -#pragma warning disable IDE1006 // Naming Styles namespace Content.Shared._DEN.Requirements.Managers; -#pragma warning restore IDE1006 // Naming Styles /// /// A manager used to check player stats against a list of requirements, getting the pass/fail status of these requirements. /// This can be used to apply restrictions to character actions, like jobs or traits. /// -public sealed partial class PlayerRequirementManager +public abstract partial class SharedPlayerRequirementManager : IPlayerRequirementManager { - /// - /// Check a context against enumerable requirements and gets the final pass/fail status of these requirements. - /// - /// The context containing fields to check against the requirements. - /// An enumerable collection of requirements. - /// Whether or not this context passes *all* requirements. If even one fails, then this is false. + /// [PublicAPI] - public static bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements) + public bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements) { foreach (var requirement in requirements) { @@ -40,4 +34,7 @@ public static bool CheckRequirements(PlayerRequirementContext context, IEnumerab return true; } + + /// + public abstract PlayerRequirementContext GetPlayerContext(ICommonSession session); } From d4ac7533bb744eff3de09d92587899801d3b712f Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:48:15 -0500 Subject: [PATCH 14/54] add "any" count requirement --- .../PlayerRequirements/CountRequirement.cs | 36 +++++++++++++++---- .../_DEN/requirements/requirement-range.ftl | 1 + 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs index a22a7d40e9..96313bc487 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs @@ -17,7 +17,7 @@ public abstract partial class CountRequirement /// Gets a string reason representation for this range. /// /// - /// This would slot into the sentence "You must have [reason] of the following items: [items]". + /// This would slot into the sentence "Must have [reason] of the following items: [items]". /// /// "At least 1", "between 2 and 5", "all" /// A string representation of this range's requirement bounds. @@ -60,7 +60,7 @@ public sealed partial class ConstantCountRequirement : CountRequirement /// public override string GetReason() { - // "You must have exactly 1 of the following items." + // "Must have exactly 1 of the following items." return Loc.GetString("count-requirement-constant-reason", ("count", Count)); } @@ -95,16 +95,16 @@ public override string GetReason() { return (Min, Max) switch { - // "You must have between 1 and 5 of the following items." + // "Must have between 1 and 5 of the following items." (not null, not null) => Loc.GetString("count-requirement-range-minmax-reason", ("minimum", Min), ("maximum", Max)), - // "You must have at most 5 of the following items." + // "Must have at most 5 of the following items." (null, not null) => Loc.GetString("count-requirement-range-maximum-reason", ("maximum", Max)), - // "You must have at least 1 of the following items." + // "Must have at least 1 of the following items." (not null, null) => Loc.GetString("count-requirement-range-minimum-reason", ("minimum", Min)), @@ -121,6 +121,30 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req } } +/// +/// To fulfill the requirement, you must have at least one item in the required collection. +/// +/// +/// This is similar to , with a Min of 1, but it says "any" in the reason instead. +/// This sounds smoother for inverted requirements; i.e. "Must not have any of the following items." +/// +public sealed partial class AllCountRequirement : CountRequirement +{ + /// + public override string GetReason() + { + // "Must have any of the following items." + return Loc.GetString("count-requirement-any-reason"); + } + + /// + public override bool CheckRequirement(IEnumerable have, IEnumerable required) + { + var count = GetFulfilledCount(have, required); + return count >= 1; + } +} + /// /// To fulfill the requirement, you must have all items in the required collection. /// @@ -129,7 +153,7 @@ public sealed partial class AllCountRequirement : CountRequirement /// public override string GetReason() { - // "You must have all of the following items." + // "Must have all of the following items." return Loc.GetString("count-requirement-all-reason"); } diff --git a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl index b869b2b179..63b2c095c6 100644 --- a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl @@ -2,4 +2,5 @@ count-requirement-constant-reason = exactly {$count} count-requirement-range-minimum-reason = at least {$minimum} count-requirement-range-maximum-reason = at most {$maximum} count-requirement-range-minmax-reason = between {$minimum} and {$maximum} +count-requirement-any-reason = any count-requirement-all-reason = all From fc85ec9615bc5a3bda590b7c731c579f05ac718e Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:48:32 -0500 Subject: [PATCH 15/54] misnamed it oops --- .../_DEN/Requirements/PlayerRequirements/CountRequirement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs index 96313bc487..9c0c152b21 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs @@ -128,7 +128,7 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req /// This is similar to , with a Min of 1, but it says "any" in the reason instead. /// This sounds smoother for inverted requirements; i.e. "Must not have any of the following items." /// -public sealed partial class AllCountRequirement : CountRequirement +public sealed partial class AnyCountRequirement : CountRequirement { /// public override string GetReason() From 06961110340c6052f83a88336e2819ec043e2eea Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:23:51 -0500 Subject: [PATCH 16/54] add GetPlayerContext() --- Content.Client/IoC/ClientContentIoC.cs | 3 ++ .../Managers/PlayerRequirementManager.cs | 31 +++++++++++++++++++ Content.Server/IoC/ServerContentIoC.cs | 3 ++ .../Managers/PlayerRequirementManager.cs | 26 ++++++++++++++++ Content.Shared/IoC/SharedContentIoC.cs | 4 +-- .../Managers/PlayerRequirementContext.cs | 2 +- 6 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 Content.Client/_DEN/Requirements/Managers/PlayerRequirementManager.cs create mode 100644 Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index efaf88b052..2ebde9222c 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -28,6 +28,8 @@ using Content.Shared.IoC; using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Players.RateLimiting; +using Content.Shared._DEN.Requirements.Managers; +using Content.Client._DEN.Requirements.Managers; namespace Content.Client.IoC { @@ -66,6 +68,7 @@ public static void Register(IDependencyCollection collection) collection.Register(); collection.Register(); collection.Register(); + collection.Register(); // DEN } } } diff --git a/Content.Client/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Client/_DEN/Requirements/Managers/PlayerRequirementManager.cs new file mode 100644 index 0000000000..c2705ef754 --- /dev/null +++ b/Content.Client/_DEN/Requirements/Managers/PlayerRequirementManager.cs @@ -0,0 +1,31 @@ +using Content.Client.Lobby; +using Content.Shared._DEN.Requirements.Managers; +using Content.Shared.Players.PlayTimeTracking; +using Robust.Client.Player; +using Robust.Shared.Player; + +namespace Content.Client._DEN.Requirements.Managers; + +/// +public sealed partial class PlayerRequirementManager : SharedPlayerRequirementManager +{ + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IClientPreferencesManager _preferences = default!; + [Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!; + + /// + public override PlayerRequirementContext GetPlayerContext(ICommonSession session) + { + if (_player.LocalSession != session) + return new(); + + var playtimes = _playtimeManager.GetPlayTimes(session); + var profile = _preferences.Preferences?.SelectedCharacter; + + return new() + { + Playtimes = playtimes, + Profile = profile, + }; + } +} diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 1c6d940e20..c57ca41539 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -1,3 +1,4 @@ +using Content.Server._DEN.Requirements.Managers; using Content.Server.Administration; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; @@ -24,6 +25,7 @@ using Content.Server.ServerUpdates; using Content.Server.Voting.Managers; using Content.Server.Worldgen.Tools; +using Content.Shared._DEN.Requirements.Managers; using Content.Shared.Administration.Logs; using Content.Shared.Administration.Managers; using Content.Shared.Chat; @@ -84,5 +86,6 @@ public static void Register(IDependencyCollection deps) deps.Register(); deps.Register(); deps.Register(); + deps.Register(); // DEN } } diff --git a/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs new file mode 100644 index 0000000000..0732f4a7a1 --- /dev/null +++ b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs @@ -0,0 +1,26 @@ +using Content.Server.GameTicking; +using Content.Shared._DEN.Requirements.Managers; +using Content.Shared.Players.PlayTimeTracking; +using Robust.Shared.Player; + +namespace Content.Server._DEN.Requirements.Managers; + +/// +public sealed partial class PlayerRequirementManager : SharedPlayerRequirementManager +{ + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!; + + /// + public override PlayerRequirementContext GetPlayerContext(ICommonSession session) + { + var playtimes = _playtimeManager.GetPlayTimes(session); + var profile = _gameTicker.GetPlayerProfile(session); + + return new() + { + Playtimes = playtimes, + Profile = profile, + }; + } +} diff --git a/Content.Shared/IoC/SharedContentIoC.cs b/Content.Shared/IoC/SharedContentIoC.cs index fd69196de0..f23b9f8355 100644 --- a/Content.Shared/IoC/SharedContentIoC.cs +++ b/Content.Shared/IoC/SharedContentIoC.cs @@ -1,5 +1,4 @@ -using Content.Shared._DEN.Requirements.Managers; -using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Markings; using Content.Shared.Localizations; namespace Content.Shared.IoC @@ -10,7 +9,6 @@ public static void Register(IDependencyCollection deps) { deps.Register(); deps.Register(); - deps.Register(); // DEN } } } diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs index 1737ea121f..d2da99b62a 100644 --- a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs +++ b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs @@ -18,5 +18,5 @@ public partial record PlayerRequirementContext /// /// A dictionary of registered playtimes for the player. /// - public Dictionary, TimeSpan>? Playtimes = null; + public IReadOnlyDictionary? Playtimes = null; } From ffbdbd956f17691fc85c0a53db452e0c0093d001 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:53:22 -0500 Subject: [PATCH 17/54] get tooltips and trait requirements working --- .../UI/Traits/EntityTraitSelector.xaml.cs | 59 ++++++++++++++++++- .../Managers/PlayerRequirementManager.cs | 15 ++++- .../_DEN/Traits/EntitySystems/TraitSystem.cs | 14 ++++- .../Preferences/HumanoidCharacterProfile.cs | 14 ++++- .../Managers/IPlayerRequirementManager.cs | 4 ++ .../PlayerRequirements/IPlayerRequirement.cs | 8 +-- .../PlayerRequirements.Playtime.cs | 35 +++++------ .../PlayerRequirements.Profile.cs | 4 +- .../Traits/Prototypes/EntityTraitPrototype.cs | 7 +++ .../_DEN/requirements/formatted-fields.ftl | 3 + .../requirements/playtime-requirements.ftl | 5 +- .../requirements/profile-requirements.ftl | 5 +- 12 files changed, 131 insertions(+), 42 deletions(-) create mode 100644 Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl diff --git a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs index 12cc197c9e..971af64d3e 100644 --- a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs +++ b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs @@ -1,8 +1,12 @@ +using System.Text; using Content.Shared._DEN.Traits.Prototypes; using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Client._DEN.Lobby.UI.Traits; @@ -19,6 +23,7 @@ public bool Preference } public ProtoId? PrototypeId { private set; get; } = null; + private EntityTraitPrototype? _trait = null; public EntityTraitSelector(EntityTraitPrototype trait) { @@ -26,12 +31,14 @@ public EntityTraitSelector(EntityTraitPrototype trait) SetTrait(trait); SelectorCheckbox.OnToggled += args => { PreferenceChanged?.Invoke(args.Pressed); }; + SelectorCheckbox.TooltipSupplier = SupplyTooltip; } private void SetTrait(EntityTraitPrototype trait) { Cost = trait.Cost; PrototypeId = trait.ID; + _trait = trait; var text = ""; @@ -40,9 +47,59 @@ private void SetTrait(EntityTraitPrototype trait) text += Loc.GetString(trait.Name); SelectorCheckbox.Text = text; + } + + /// + /// Provide a tooltip to this control if it has a valid trait set. + /// This includes the trait's description and its requirement text. + /// + /// A tooltip with the trait's description, if this control has a trait. + private Tooltip? SupplyTooltip(Control sender) + { + if (_trait is null) + return null; + + var tooltip = new Tooltip(); + var tooltipString = ConstructTooltipDescription(_trait); + + if (FormattedMessage.TryFromMarkup(tooltipString, out var msg)) + tooltip.SetMessage(msg); + + return tooltip; + } + + /// + /// Construct a full tooltip description for a given trait. + /// + /// The trait to construct a description out of. + /// The tooltip text associated with this trait. + private static string ConstructTooltipDescription(EntityTraitPrototype trait) + { + var tooltipBuilder = new StringBuilder(); if (trait.Description is not null) - SelectorCheckbox.ToolTip = Loc.GetString(trait.Description.Value); + tooltipBuilder.AppendLine(Loc.GetString(trait.Description.Value)); + + if (trait.Requirements.Count > 0) + { + tooltipBuilder.AppendLine(); // Empty line + foreach (var requirement in trait.Requirements) + { + var reason = requirement.GetReason(); + if (reason is null) + continue; + + tooltipBuilder.AppendLine(reason); + } + } + + var tooltipString = tooltipBuilder.ToString(); + var lineBreak = "\n"; + + if (tooltipString.EndsWith(lineBreak)) + tooltipString = tooltipString[..^lineBreak.Length]; + + return tooltipString; } public void SetInvalid(bool invalid) diff --git a/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs index 0732f4a7a1..63d27ebe68 100644 --- a/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs +++ b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs @@ -1,6 +1,7 @@ -using Content.Server.GameTicking; +using Content.Server.Preferences.Managers; using Content.Shared._DEN.Requirements.Managers; using Content.Shared.Players.PlayTimeTracking; +using Content.Shared.Preferences; using Robust.Shared.Player; namespace Content.Server._DEN.Requirements.Managers; @@ -8,14 +9,14 @@ namespace Content.Server._DEN.Requirements.Managers; /// public sealed partial class PlayerRequirementManager : SharedPlayerRequirementManager { - [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!; + [Dependency] private readonly IServerPreferencesManager _prefsManager = default!; /// public override PlayerRequirementContext GetPlayerContext(ICommonSession session) { var playtimes = _playtimeManager.GetPlayTimes(session); - var profile = _gameTicker.GetPlayerProfile(session); + var profile = GetProfile(session); return new() { @@ -23,4 +24,12 @@ public override PlayerRequirementContext GetPlayerContext(ICommonSession session Profile = profile, }; } + + private HumanoidCharacterProfile? GetProfile(ICommonSession session) + { + if (!_prefsManager.TryGetCachedPreferences(session.UserId, out var prefs)) + return null; + + return prefs.SelectedCharacter; + } } diff --git a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs index 997672410d..9c3088e1a0 100644 --- a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs +++ b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs @@ -1,10 +1,8 @@ +using Content.Shared._DEN.Requirements.Managers; using Content.Shared._DEN.Traits.EntitySystems; -using Content.Shared._DEN.Traits.Prototypes; using Content.Shared.GameTicking; -using Content.Shared.Hands.EntitySystems; using Content.Shared.Humanoid; using Content.Shared.Roles; -using Content.Shared.Whitelist; using Robust.Shared.Prototypes; #pragma warning disable IDE1006 // Naming Styles @@ -14,6 +12,7 @@ namespace Content.Server._DEN.Traits.EntitySystems; public sealed partial class TraitSystem : SharedTraitSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IPlayerRequirementManager _requirements = default!; public override void Initialize() { @@ -45,6 +44,7 @@ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args) continue; } + // TODO DEN: Remove if (trait.AllowedSpecies != null) { if (!TryComp(mob, out var profile) @@ -55,6 +55,14 @@ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args) } } + var context = _requirements.GetPlayerContext(args.Player); + context.Profile = args.Profile; + if (!_requirements.CheckRequirements(context, trait.Requirements)) + { + Log.Error($"Tried to spawn trait {traitId} on {ToPrettyString(mob)}, but we failed the requirements to do so!"); + continue; + } + TryAddTrait(mob, traitId, out _); } } diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index e117771313..4c26451021 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -21,6 +21,7 @@ using Robust.Shared; using YamlDotNet.RepresentationModel; using Content.Shared._DEN.Traits.Prototypes; +using Content.Shared._DEN.Requirements.Managers; namespace Content.Shared.Preferences { @@ -489,6 +490,7 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection { var configManager = collection.Resolve(); var prototypeManager = collection.Resolve(); + var requirements = collection.Resolve(); // DEN if (!prototypeManager.TryIndex(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false) { @@ -605,15 +607,21 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection .Where(id => prototypeManager.TryIndex(id, out var antag) && antag.SetPreference) .ToList(); + // Begin DEN: Trait validation // var traits = TraitPreferences // .Where(prototypeManager.HasIndex) - // .ToList(); // DEN + // .ToList(); + + var context = requirements.GetPlayerContext(session); + context.Profile = this; var traits = EntityTraitPreferences .Where(t => prototypeManager.TryIndex(t, out var trait) && trait.Selectable - && (trait.AllowedSpecies is null || trait.AllowedSpecies.Contains(Species))) - .ToList(); // DEN + && (trait.AllowedSpecies is null || trait.AllowedSpecies.Contains(Species)) // TODO DEN: Remove + && requirements.CheckRequirements(context, trait.Requirements)) + .ToList(); + // End DEN Name = name; FlavorText = flavortext; diff --git a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs index 03dd15e39d..234ade55fd 100644 --- a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs +++ b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs @@ -3,6 +3,10 @@ namespace Content.Shared._DEN.Requirements.Managers; +/// +/// A manager used to check player stats against a list of requirements, getting the pass/fail status of these requirements. +/// This can be used to apply restrictions to character actions, like jobs or traits. +/// public interface IPlayerRequirementManager { /// diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs index 2458beefec..32edd51361 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs @@ -58,12 +58,10 @@ public partial interface IPlayerRequirement /// Get a localized string representing how this requirement displays to players. /// /// - /// This should, ideally, display regardless if the context actually passes or not. - /// It should also display differently depending on the value of . + /// This should display differently depending on the value of . /// - /// A definition of parameters to check against the requirement. /// An optional string representing the requirement text to display to a player. - string? GetReason(PlayerRequirementContext context); + string? GetReason(); } /// @@ -81,7 +79,7 @@ public abstract partial class PlayerRequirement : IPlayerRequirement public abstract bool CheckRequirement(PlayerRequirementContext context); /// - public abstract string? GetReason(PlayerRequirementContext context); + public abstract string? GetReason(); /// public abstract bool PreCheck(PlayerRequirementContext context); diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs index b22bc123e3..941dc089f4 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -1,5 +1,6 @@ using Content.Shared._DEN.Requirements.Managers; using Content.Shared.CCVar; +using Content.Shared.Localizations; using Content.Shared.Roles; using Robust.Shared.Configuration; using Robust.Shared.Prototypes; @@ -72,17 +73,13 @@ protected static bool ShouldAutoPass() /// protected string? GetPlaytimeConstraintReason() { - var config = IoCManager.Resolve(); - if (!config.GetCVar(CCVars.GameRoleTimers)) - return null; - if (MinTime == null && MaxTime == null) return null; var minTimeString = FormatPlaytime(MinTime); var maxTimeString = FormatPlaytime(MaxTime); - return (MinTime, MaxTime) switch + return (minTimeString, maxTimeString) switch { (not null, not null) => Loc.GetString("player-requirement-playtime-minmax-time", ("minimum", minTimeString), ("maximum", maxTimeString)), @@ -97,18 +94,14 @@ protected static bool ShouldAutoPass() }; } - /// - /// Gets a localized time string for the given playtime. - /// - /// The playtime to format. - /// A localized time string for this playtime. - private static string FormatPlaytime(TimeSpan? playtime) + private static string? FormatPlaytime(TimeSpan? playtime) { - var time = ((int?)playtime?.TotalMinutes) ?? 0; + if (playtime is null) + return null; - return playtime != null - ? Loc.GetString("player-requirement-playtime-time", ("playtime", time)) - : string.Empty; + var playtimeString = ContentLocalizationManager.FormatPlaytime(playtime.Value); + return Loc.GetString("player-requirement-format-time", + ("playtime", playtimeString)); } } @@ -138,18 +131,22 @@ public override bool CheckRequirement(PlayerRequirementContext context) } /// - public override string? GetReason(PlayerRequirementContext context) + public override string? GetReason() { // Do not give a reason if role timers are disabled. if (ShouldAutoPass()) return null; - // Get the department name. + // Get the department name and format it with a color. var protoMan = IoCManager.Resolve(); if (!protoMan.TryIndex(Department, out var department)) return null; var deptName = Loc.GetString(department.Name); + var deptColor = department.Color.ToHex(); + var formattedDept = Loc.GetString("player-requirement-format-department", + ("color", deptColor), + ("department", deptName)); // Get the playtime constraint string. var playtimeString = GetPlaytimeConstraintReason(); @@ -160,10 +157,10 @@ public override bool CheckRequirement(PlayerRequirementContext context) ("inverted", Inverted), ("timeConstraint", playtimeString)); - // E.g. "You must have 120 minutes in the Science department." + // E.g. "You must have 2h30m in the Science department." return Loc.GetString("player-requirement-department-playtime-reason", ("constraint", constraintReason), - ("department", deptName)); + ("department", formattedDept)); } /// diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs index 129464701b..df482d9390 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs @@ -36,7 +36,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) } /// - public override string? GetReason(PlayerRequirementContext context) + public override string? GetReason() { var protoMan = IoCManager.Resolve(); var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan)); @@ -62,6 +62,6 @@ private static string LocalizeTrait(ProtoId traitId, IProt if (protoMan.TryIndex(traitId, out var trait)) traitName = Loc.GetString(trait.Name); - return Loc.GetString("player-requirement-trait", ("traitName", traitName)); + return Loc.GetString("player-requirement-format-trait", ("trait", traitName)); } } diff --git a/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs b/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs index 70e6fc7dde..40cc398f54 100644 --- a/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs +++ b/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared._DEN.Requirements.PlayerRequirements; using Content.Shared._DEN.Traits.TraitFunctions; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Traits; @@ -69,4 +70,10 @@ public sealed partial class EntityTraitPrototype : IPrototype /// [DataField("characterEditorSelectable")] public bool Selectable = true; + + /// + /// A list of requirements to use this trait. + /// + [DataField] + public List Requirements = new(); } diff --git a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl new file mode 100644 index 0000000000..891ed33750 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl @@ -0,0 +1,3 @@ +player-requirement-format-department = [color={$color}]{$department}[/color] +player-requirement-format-time = [color=yellow]{$playtime}[/color] +player-requirement-format-trait = [color=LightBlue]{$trait}[/color] diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl index 1b7a38a63b..9775b9a01d 100644 --- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl @@ -1,10 +1,9 @@ -player-requirement-playtime-time = {$playtime} minutes player-requirement-playtime-minimum-time = at least {$playtime} player-requirement-playtime-maximum-time = less than {$playtime} player-requirement-playtime-minmax-time = between {$minimum} and {$maximum} player-requirement-playtime-constraint-reason = Must{$inverted -> - [true] not - *[false] {" "} + [true] {" "}not + *[false] {""} } have {$timeConstraint} player-requirement-department-playtime-reason = {$constraint} in the {$department} department. diff --git a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl index 79abfd63bb..970eb2759b 100644 --- a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl @@ -1,5 +1,4 @@ -player-requirement-trait = [color=LightBlue]{$traitName}[/color] player-requirement-trait-reason = Must{$inverted -> - [true] not + [true] {" "}not *[false] {" "} -} have {$constraint} of these traits: {$traits}; +} have {$constraint} of these traits: {$traits} From 0405af2dcac259fa00b0a031531ad8141bf39a10 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:16:41 -0500 Subject: [PATCH 18/54] clean up tooltip code --- .../Lobby/UI/Traits/EntityTraitSelector.xaml.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs index 971af64d3e..b590fa0cff 100644 --- a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs +++ b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs @@ -59,9 +59,11 @@ private void SetTrait(EntityTraitPrototype trait) if (_trait is null) return null; - var tooltip = new Tooltip(); var tooltipString = ConstructTooltipDescription(_trait); + if (tooltipString.Length == 0) + return null; + var tooltip = new Tooltip(); if (FormattedMessage.TryFromMarkup(tooltipString, out var msg)) tooltip.SetMessage(msg); @@ -77,9 +79,11 @@ private static string ConstructTooltipDescription(EntityTraitPrototype trait) { var tooltipBuilder = new StringBuilder(); + // Add description if (trait.Description is not null) tooltipBuilder.AppendLine(Loc.GetString(trait.Description.Value)); + // Add requirement reason texts if (trait.Requirements.Count > 0) { tooltipBuilder.AppendLine(); // Empty line @@ -93,12 +97,7 @@ private static string ConstructTooltipDescription(EntityTraitPrototype trait) } } - var tooltipString = tooltipBuilder.ToString(); - var lineBreak = "\n"; - - if (tooltipString.EndsWith(lineBreak)) - tooltipString = tooltipString[..^lineBreak.Length]; - + var tooltipString = tooltipBuilder.ToString().Trim(); return tooltipString; } From b45bab7818cafb63bfadd208b162b43114b7dba8 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:16:47 -0500 Subject: [PATCH 19/54] add species requirements --- .../PlayerRequirements.Profile.cs | 62 +++++++++++++++++++ .../_DEN/requirements/formatted-fields.ftl | 3 +- .../requirements/profile-requirements.ftl | 7 ++- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs index df482d9390..2fbfd17e05 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs @@ -1,6 +1,7 @@ using System.Linq; using Content.Shared._DEN.Requirements.Managers; using Content.Shared._DEN.Traits.Prototypes; +using Content.Shared.Humanoid.Prototypes; using Robust.Shared.Prototypes; namespace Content.Shared._DEN.Requirements.PlayerRequirements; @@ -65,3 +66,64 @@ private static string LocalizeTrait(ProtoId traitId, IProt return Loc.GetString("player-requirement-format-trait", ("trait", traitName)); } } + +/// +/// Checks if a player's character is one of the following species. +/// +public sealed partial class PlayerSpeciesRequirement : PlayerRequirement +{ + /// + /// Character's profile must be one of these species to pass the requirement. + /// + [DataField] + public HashSet> Species = new(); + + /// + public override bool PreCheck(PlayerRequirementContext context) + { + return context.Profile != null; + } + + /// + public override bool CheckRequirement(PlayerRequirementContext context) + { + if (context.Profile == null) + return false; + + return Species.Contains(context.Profile.Species); + } + + /// + public override string? GetReason() + { + var protoMan = IoCManager.Resolve(); + + // Add all species names to a list + var speciesList = new List(); + foreach (var speciesId in Species) + speciesList.Add(LocalizeSpecies(speciesId, protoMan)); + + var joinedSpecies = string.Join(", ", speciesList); + + // "Must be one of these species: Human, Dwarf, Slimeperson" + return Loc.GetString("player-requirement-species-reason", + ("inverted", Inverted), + ("species", joinedSpecies)); + } + + /// + /// Localizes a species ID into a formatted species name. + /// + /// The ID of the species. + /// The prototype manager. + /// A formatted species name string. + private static string LocalizeSpecies(ProtoId speciesId, IPrototypeManager protoMan) + { + var speciesName = speciesId; + if (!protoMan.TryIndex(speciesId, out var species)) + return speciesName; + + speciesName = Loc.GetString(species.Name); + return Loc.GetString("player-requirement-format-species", ("species", speciesName)); + } +} diff --git a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl index 891ed33750..e5fdb2b215 100644 --- a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl @@ -1,3 +1,4 @@ player-requirement-format-department = [color={$color}]{$department}[/color] -player-requirement-format-time = [color=yellow]{$playtime}[/color] +player-requirement-format-species = [color=Green]{$species}[/color] +player-requirement-format-time = [color=Yellow]{$playtime}[/color] player-requirement-format-trait = [color=LightBlue]{$trait}[/color] diff --git a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl index 970eb2759b..02bce0b096 100644 --- a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl @@ -1,4 +1,9 @@ player-requirement-trait-reason = Must{$inverted -> [true] {" "}not - *[false] {" "} + *[false] {""} } have {$constraint} of these traits: {$traits} + +player-requirement-species-reason = Must{$inverted -> + [true] {" "}not + *[false] {""} +} be one of these species: {$species} From 9cf0f61b4735a67d369684ba801a0d4822f83f7a Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:21:16 -0500 Subject: [PATCH 20/54] add a missing comment --- .../_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs index 32edd51361..8a0bc09d9d 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs @@ -44,7 +44,7 @@ public partial interface IPlayerRequirement /// . /// /// A definition of parameters to check against the requirement. - /// + /// Whether or not the context has all required parameters. bool PreCheck(PlayerRequirementContext context); /// From 58122c5df45385ed81d879eb650604d353a6dd00 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:26:06 -0500 Subject: [PATCH 21/54] comment this method --- .../_DEN/Requirements/Managers/PlayerRequirementManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs index 63d27ebe68..ea93973b6b 100644 --- a/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs +++ b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs @@ -25,6 +25,11 @@ public override PlayerRequirementContext GetPlayerContext(ICommonSession session }; } + /// + /// Retrieves a session's selected lobby character. + /// + /// The session to retrieve a profile for. + /// The session's currently-selected profile, if any. private HumanoidCharacterProfile? GetProfile(ICommonSession session) { if (!_prefsManager.TryGetCachedPreferences(session.UserId, out var prefs)) From 3522c3233ef74e6cb02c3a2accc05d5a7dabdf9c Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:45:44 -0500 Subject: [PATCH 22/54] simplify this realized it doesnt need to be removed from the list if it fails requirements because requirements are contextual --- Content.Shared/Preferences/HumanoidCharacterProfile.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index 4c26451021..86252c1a53 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -616,10 +616,7 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection context.Profile = this; var traits = EntityTraitPreferences - .Where(t => prototypeManager.TryIndex(t, out var trait) - && trait.Selectable - && (trait.AllowedSpecies is null || trait.AllowedSpecies.Contains(Species)) // TODO DEN: Remove - && requirements.CheckRequirements(context, trait.Requirements)) + .Where(t => prototypeManager.TryIndex(t, out var trait) && trait.Selectable) .ToList(); // End DEN From 3650bc95cce46b1d9d1973e8bc304e8a110359b3 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 04:05:27 -0500 Subject: [PATCH 23/54] add hiding traits from UI --- .../UI/Traits/EntityTraitSelector.xaml.cs | 21 ++++++++ .../Managers/IPlayerRequirementManager.cs | 9 ++++ .../SharedPlayerRequirementManager.cs | 49 +++++++++++++------ .../PlayerRequirements/IPlayerRequirement.cs | 9 ++++ 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs index b590fa0cff..2d9c28351b 100644 --- a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs +++ b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs @@ -1,6 +1,8 @@ using System.Text; +using Content.Shared._DEN.Requirements.Managers; using Content.Shared._DEN.Traits.Prototypes; using Robust.Client.AutoGenerated; +using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; @@ -13,6 +15,9 @@ namespace Content.Client._DEN.Lobby.UI.Traits; [GenerateTypedNameReferences] public sealed partial class EntityTraitSelector : BoxContainer { + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IPlayerRequirementManager _requirements = default!; + public event Action? PreferenceChanged; public int Cost { private set; get; } = 0; @@ -28,6 +33,7 @@ public bool Preference public EntityTraitSelector(EntityTraitPrototype trait) { RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); SetTrait(trait); SelectorCheckbox.OnToggled += args => { PreferenceChanged?.Invoke(args.Pressed); }; @@ -47,6 +53,21 @@ private void SetTrait(EntityTraitPrototype trait) text += Loc.GetString(trait.Name); SelectorCheckbox.Text = text; + UpdateVisibility(trait); + } + + /// + /// Toggles the visibility of this selector, depending on the requirements of a given trait. + /// + /// The trait to check the requirements of. + private void UpdateVisibility(EntityTraitPrototype trait) + { + var session = _player.LocalSession; + if (session == null) + return; + + var context = _requirements.GetPlayerContext(session); + Visible = !_requirements.ShouldHide(context, trait.Requirements); } /// diff --git a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs index 234ade55fd..297d3b03f6 100644 --- a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs +++ b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs @@ -17,6 +17,15 @@ public interface IPlayerRequirementManager /// Whether or not this context passes *all* requirements. If even one fails, then this is false. bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements); + /// + /// Whether or not the item associated with a set of requirements should be hidden + /// from the player, such as in UI. + /// + /// The context containing fields to check against the requirements. + /// An enumerable collection of requirements. + /// Whether or not the item should be hidden from the player. + bool ShouldHide(PlayerRequirementContext context, IEnumerable requirements); + /// /// Creates a new PlayerRequirementContext with context fields pre-filled. /// diff --git a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs index 4a317b521c..3205637ae7 100644 --- a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs +++ b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs @@ -15,22 +15,41 @@ public abstract partial class SharedPlayerRequirementManager : IPlayerRequiremen public bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements) { foreach (var requirement in requirements) - { - // Pre-check the requirement. This ensures our context has all the fields needed for the requirement. - // If the pre-check fails, whether or not this requirement passes depends on requirement.MustPassPreCheck. - // If you don't need to pass the pre-check, then it's an auto-success. - if (!requirement.PreCheck(context)) - { - if (requirement.MustPassPreCheck) - return false; - - continue; - } - - // Check the actual requirement, now. - if (!requirement.CheckRequirement(context)) + if (!CheckRequirement(context, requirement)) return false; - } + + return true; + } + + /// + [PublicAPI] + public bool ShouldHide(PlayerRequirementContext context, IEnumerable requirements) + { + foreach (var requirement in requirements) + if (!CheckRequirement(context, requirement) && requirement.HideIfFailed) + return true; + + return false; + } + + /// + /// Check a single requirement for whether it passes/fails against a context. + /// + /// The context containing fields to check against the requirement. + /// The requirement to check. + /// Whether this context passes the requirement. + [PublicAPI] + public static bool CheckRequirement(PlayerRequirementContext context, IPlayerRequirement requirement) + { + // Pre-check the requirement. This ensures our context has all the fields needed for the requirement. + // If the pre-check fails, whether or not this requirement passes depends on requirement.MustPassPreCheck. + // If you don't need to pass the pre-check, then it's an auto-success. + if (!requirement.PreCheck(context)) + return !requirement.MustPassPreCheck; + + // Check the actual requirement, now. + if (!requirement.CheckRequirement(context)) + return false; return true; } diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs index 8a0bc09d9d..5aeae911d5 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs @@ -35,6 +35,12 @@ public partial interface IPlayerRequirement [DataField] bool MustPassPreCheck { get; set; } + /// + /// Whether or not the item should be hidden from the UI if this requirement fails. + /// + [DataField] + bool HideIfFailed { get; set; } + /// /// Check if the given context has all parameters required in order to perform an actual check. /// @@ -75,6 +81,9 @@ public abstract partial class PlayerRequirement : IPlayerRequirement /// [DataField] public bool MustPassPreCheck { get; set; } = false; + /// + [DataField] public bool HideIfFailed { get; set; } = false; + /// public abstract bool CheckRequirement(PlayerRequirementContext context); From b6f862e9f571d5dae18f63dfcf8c208e980320da Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 04:07:09 -0500 Subject: [PATCH 24/54] remove AllowedSpecies from EntityTraitPrototype --- .../_DEN/Lobby/UI/Traits/TraitCategoryBox.xaml.cs | 3 +-- .../_DEN/Traits/EntitySystems/TraitSystem.cs | 11 ----------- .../_DEN/Traits/Prototypes/EntityTraitPrototype.cs | 7 ------- 3 files changed, 1 insertion(+), 20 deletions(-) diff --git a/Content.Client/_DEN/Lobby/UI/Traits/TraitCategoryBox.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/TraitCategoryBox.xaml.cs index 5a440bae60..32926df911 100644 --- a/Content.Client/_DEN/Lobby/UI/Traits/TraitCategoryBox.xaml.cs +++ b/Content.Client/_DEN/Lobby/UI/Traits/TraitCategoryBox.xaml.cs @@ -46,8 +46,7 @@ public void SetTraits(List traits) foreach (var trait in traits) { - if (!trait.Selectable || trait.AllowedSpecies is not null - && (_profile?.Species is null || !trait.AllowedSpecies.Contains(_profile.Species))) + if (!trait.Selectable) continue; var selector = new EntityTraitSelector(trait); diff --git a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs index 9c3088e1a0..ae503dc1d4 100644 --- a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs +++ b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs @@ -44,17 +44,6 @@ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args) continue; } - // TODO DEN: Remove - if (trait.AllowedSpecies != null) - { - if (!TryComp(mob, out var profile) - || !trait.AllowedSpecies.Contains(profile.Species)) - { - Log.Error($"Tried to spawn trait {traitId} on {ToPrettyString(mob)} with invalid species: {profile?.Species ?? "null"}!"); - continue; - } - } - var context = _requirements.GetPlayerContext(args.Player); context.Profile = args.Profile; if (!_requirements.CheckRequirements(context, trait.Requirements)) diff --git a/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs b/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs index 40cc398f54..9553ca6a4e 100644 --- a/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs +++ b/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs @@ -52,13 +52,6 @@ public sealed partial class EntityTraitPrototype : IPrototype [DataField] public ProtoId? Category; - /// - /// A list of species that allowed to take this trait. If null, then all species may take it. - /// TODO: Replace with a more robust "requirement" system. - /// - [DataField] - public HashSet>? AllowedSpecies = null; - /// /// A list of functions associated with this trait. /// From 21349cc330a65d3d380e8cd1466f145c46833791 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 04:07:54 -0500 Subject: [PATCH 25/54] add species requirements to morphology traits --- .../Prototypes/_DEN/EntityTraits/species.yml | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Resources/Prototypes/_DEN/EntityTraits/species.yml b/Resources/Prototypes/_DEN/EntityTraits/species.yml index 71eae2c81b..d6a1610228 100644 --- a/Resources/Prototypes/_DEN/EntityTraits/species.yml +++ b/Resources/Prototypes/_DEN/EntityTraits/species.yml @@ -8,7 +8,10 @@ name: trait-morph-human-canid-name description: trait-morph-human-canid-desc category: SpeciesMorphs - allowedSpecies: [Human] + requirements: + - !type:PlayerSpeciesRequirement + hideIfFailed: true + species: [Human] cost: 1 whitelist: components: @@ -22,7 +25,10 @@ name: trait-morph-human-felinid-name description: trait-morph-human-felinid-desc category: SpeciesMorphs - allowedSpecies: [Human] + requirements: + - !type:PlayerSpeciesRequirement + hideIfFailed: true + species: [Human] cost: 1 whitelist: components: @@ -36,7 +42,10 @@ name: trait-morph-human-kitsune-name description: trait-morph-human-kitsune-desc category: SpeciesMorphs - allowedSpecies: [Human] + requirements: + - !type:PlayerSpeciesRequirement + hideIfFailed: true + species: [Human] cost: 1 whitelist: components: @@ -50,7 +59,10 @@ name: trait-morph-human-oni-name description: trait-morph-human-oni-desc category: SpeciesMorphs - allowedSpecies: [Human] + requirements: + - !type:PlayerSpeciesRequirement + hideIfFailed: true + species: [Human] cost: 1 whitelist: components: From fb11510174c03faccfed9e90cb4e553bc16cce28 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:54:36 -0500 Subject: [PATCH 26/54] obsolete this --- Content.Shared/Roles/JobRequirements.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Content.Shared/Roles/JobRequirements.cs b/Content.Shared/Roles/JobRequirements.cs index 62d50f8489..488d7a2cc4 100644 --- a/Content.Shared/Roles/JobRequirements.cs +++ b/Content.Shared/Roles/JobRequirements.cs @@ -6,6 +6,7 @@ namespace Content.Shared.Roles; +[Obsolete("Use PlayerRequirements instead")] // DEN public static class JobRequirements { /// @@ -62,6 +63,7 @@ public static bool TryRequirementsMet( /// [ImplicitDataDefinitionForInheritors] [Serializable, NetSerializable] +[Obsolete("Use PlayerRequirements instead")] // DEN public abstract partial class JobRequirement { [DataField] From 9ee17ee2ada5409c22deafd5ca9e4d5ffb7f04d8 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:25:36 -0500 Subject: [PATCH 27/54] add job playtime requirements --- .../PlayerRequirements.Playtime.cs | 132 ++++++++++++++++-- .../_DEN/requirements/formatted-fields.ftl | 1 + .../requirements/playtime-requirements.ftl | 3 +- 3 files changed, 124 insertions(+), 12 deletions(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs index 941dc089f4..9f8f928061 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -2,6 +2,7 @@ using Content.Shared.CCVar; using Content.Shared.Localizations; using Content.Shared.Roles; +using Content.Shared.Roles.Jobs; using Robust.Shared.Configuration; using Robust.Shared.Prototypes; @@ -137,16 +138,9 @@ public override bool CheckRequirement(PlayerRequirementContext context) if (ShouldAutoPass()) return null; - // Get the department name and format it with a color. + // Get a formatted department name. var protoMan = IoCManager.Resolve(); - if (!protoMan.TryIndex(Department, out var department)) - return null; - - var deptName = Loc.GetString(department.Name); - var deptColor = department.Color.ToHex(); - var formattedDept = Loc.GetString("player-requirement-format-department", - ("color", deptColor), - ("department", deptName)); + var deptName = FormatDepartment(protoMan); // Get the playtime constraint string. var playtimeString = GetPlaytimeConstraintReason(); @@ -157,10 +151,29 @@ public override bool CheckRequirement(PlayerRequirementContext context) ("inverted", Inverted), ("timeConstraint", playtimeString)); - // E.g. "You must have 2h30m in the Science department." + // E.g. "You must have 2h30m of playtime in the Science department." return Loc.GetString("player-requirement-department-playtime-reason", ("constraint", constraintReason), - ("department", formattedDept)); + ("department", deptName)); + } + + /// + /// Format this requirement's department name, with a color. + /// + /// The prototype manager. + /// The department name of this prototype, formatted. + private string FormatDepartment(IPrototypeManager protoMan) + { + if (!protoMan.TryIndex(Department, out var department)) + return Department; + + var deptName = Loc.GetString(department.Name); + var deptColor = department.Color.ToHex(); + var formattedDept = Loc.GetString("player-requirement-format-department", + ("color", deptColor), + ("department", deptName)); + + return formattedDept; } /// @@ -192,3 +205,100 @@ public override bool CheckRequirement(PlayerRequirementContext context) return playtime; } } + +/// +/// Checks if a player's total playtime in a given job fits within a given playtime range. +/// +public sealed partial class PlayerJobPlaytimeRequirement : PlayerPlaytimeRequirement +{ + /// + /// The job we should check against the requirement. + /// + [DataField(required: true)] + public ProtoId Job = default!; + + /// + public override bool CheckRequirement(PlayerRequirementContext context) + { + // Auto-pass if role timers are disabled. + if (ShouldAutoPass()) + return true; + + var playtime = GetJobPlaytime(context); + if (playtime is null) + return false; + + return IsValidPlaytime(playtime.Value); + } + + /// + public override string? GetReason() + { + // Do not give a reason if role timers are disabled. + if (ShouldAutoPass()) + return null; + + // Get the job name and format it with a color. + var protoMan = IoCManager.Resolve(); + var jobName = FormatJob(protoMan); + + // Get the playtime constraint string. + var playtimeString = GetPlaytimeConstraintReason(); + if (playtimeString == null) + return null; + + var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason", + ("inverted", Inverted), + ("timeConstraint", playtimeString)); + + // E.g. "You must have 2h30m of playtime as a Mime." + return Loc.GetString("player-requirement-job-playtime-reason", + ("constraint", constraintReason), + ("job", jobName)); + } + + /// + /// Format this requirement's job name, with a color. + /// + /// The prototype manager. + /// The department name of this prototype, formatted. + private string FormatJob(IPrototypeManager protoMan) + { + if (!protoMan.TryIndex(Job, out var job)) + return Job; + + var jobName = Loc.GetString(job.Name); + + // Gotta use the department to recolor this role's name. + var entMan = IoCManager.Resolve(); + var jobSystem = entMan.System(); + var deptColor = Color.LightGray.ToHex(); + if (jobSystem.TryGetPrimaryDepartment(Job, out var dept) || jobSystem.TryGetDepartment(Job, out dept)) + deptColor = dept.Color.ToHex(); + + var formattedJob = Loc.GetString("player-requirement-format-job", + ("color", deptColor), + ("job", jobName)); + + return formattedJob; + } + + /// + /// Get the total playtime for this job. + /// + /// A definition of parameters to check against the requirement. + /// The total playtime of this job. Null if either context playtimes or job is invalid. + private TimeSpan? GetJobPlaytime(PlayerRequirementContext context) + { + var protoMan = IoCManager.Resolve(); + var playtime = TimeSpan.Zero; + + if (context.Playtimes == null || !protoMan.TryIndex(Job, out var job)) + return null; + + if (context.Playtimes.TryGetValue(job.PlayTimeTracker, out var tracker)) + playtime = tracker; + + return playtime; + } +} diff --git a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl index e5fdb2b215..ee4511acc6 100644 --- a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl @@ -1,4 +1,5 @@ player-requirement-format-department = [color={$color}]{$department}[/color] +player-requirement-format-job = [color={$color}]{$job}[/color] player-requirement-format-species = [color=Green]{$species}[/color] player-requirement-format-time = [color=Yellow]{$playtime}[/color] player-requirement-format-trait = [color=LightBlue]{$trait}[/color] diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl index 9775b9a01d..d5516658ae 100644 --- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl @@ -4,6 +4,7 @@ player-requirement-playtime-minmax-time = between {$minimum} and {$maximum} player-requirement-playtime-constraint-reason = Must{$inverted -> [true] {" "}not *[false] {""} -} have {$timeConstraint} +} have {$timeConstraint} of playtime player-requirement-department-playtime-reason = {$constraint} in the {$department} department. +player-requirement-job-playtime-reason = {$constraint} as a {$job}. From e06f5d572900257d36a050684d921241664a1afc Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:04:44 -0500 Subject: [PATCH 28/54] Turn min/max ranges into IPlayerRangeRequirement --- .../PlayerRequirements/CountRequirement.cs | 50 ++++----- .../PlayerRequirements/IPlayerRequirement.cs | 74 +++++++++++++ .../PlayerRequirements.Playtime.cs | 102 +++++++++--------- .../_DEN/requirements/requirement-range.ftl | 7 +- 4 files changed, 154 insertions(+), 79 deletions(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs index 9c0c152b21..0f0a105f17 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs @@ -76,48 +76,50 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req /// /// To fulfill this requirement, you must have a number of items between two values, or either a minimum / maximum. /// -public sealed partial class RangeCountRequirement : CountRequirement +public sealed partial class RangeCountRequirement : CountRequirement, IPlayerRangeRequirement { /// - /// Minimum amount of required items you need. + /// Minimum amount of required items you need. Null means no minimum constraint. /// [DataField] - public int? Min = null; + public int? Min { get; set; } = null; /// - /// Maximum amount of required items you can have. + /// Maximum amount of required items you can have. Null means no maximum constraint. /// [DataField] - public int? Max = null; + public int? Max { get; set; } = null; /// public override string GetReason() { - return (Min, Max) switch - { - // "Must have between 1 and 5 of the following items." - (not null, not null) => Loc.GetString("count-requirement-range-minmax-reason", - ("minimum", Min), - ("maximum", Max)), - - // "Must have at most 5 of the following items." - (null, not null) => Loc.GetString("count-requirement-range-maximum-reason", - ("maximum", Max)), - - // "Must have at least 1 of the following items." - (not null, null) => Loc.GetString("count-requirement-range-minimum-reason", - ("minimum", Min)), - - _ => string.Empty - }; + if (this is IPlayerRangeRequirement range) + return range.GetRangeConstraintReason(); + + return string.Empty; } /// public override bool CheckRequirement(IEnumerable have, IEnumerable required) { var count = GetFulfilledCount(have, required); - return (Min == null || count >= Min) - && (Max == null || count <= Max); + + if (this is IPlayerRangeRequirement range) + return range.IsInRange(count); + + return false; + } + + /// + public string? GetMinText() + { + return Min?.ToString(); + } + + /// + public string? GetMaxText() + { + return Max?.ToString(); } } diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs index 5aeae911d5..5180944eea 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Shared._DEN.Requirements.Managers; namespace Content.Shared._DEN.Requirements.PlayerRequirements; @@ -93,3 +94,76 @@ public abstract partial class PlayerRequirement : IPlayerRequirement /// public abstract bool PreCheck(PlayerRequirementContext context); } + +/// +/// An interface representing a requirement where the value is between a given minimum +/// and/or maximum range. +/// +/// The value of the range to use. +public interface IPlayerRangeRequirement where T : struct, IComparable +{ + /// + /// The optional minimum value of this requirement to pass. + /// + T? Min { get; set; } + + /// + /// The optional maximum value of this requirement to pass. + /// + T? Max { get; set; } + + /// + /// Check if a value is within range. + /// + /// The value to check. + /// Whether this value is in range or not. + bool IsInRange(T value) + { + // Value is less than minimum + if (Min != null && value.CompareTo(Min) < 0) + return false; + + // Value is greater than maximum + if (Max != null && value.CompareTo(Max) > 0) + return false; + + return true; + } + + /// + /// Get a reason message to display to the player for this requirement's allowed value range. + /// + string GetRangeConstraintReason() + { + var minText = GetMinText(); + var maxText = GetMaxText(); + + return (minText, maxText) switch + { + // "Must have between 1 and 5 of the following items." + (not null, not null) => Loc.GetString("player-requirement-range-minmax-reason", + ("minimum", minText), + ("maximum", maxText)), + + // "Must have at most 5 of the following items." + (null, not null) => Loc.GetString("player-requirement-range-maximum-reason", + ("maximum", maxText)), + + // "Must have at least 1 of the following items." + (not null, null) => Loc.GetString("player-requirement-range-minimum-reason", + ("minimum", minText)), + + _ => string.Empty + }; + } + + /// + /// Get a string representation of this range's minimum value. + /// + string? GetMinText(); + + /// + /// Get a string representation of this range's maximum value. + /// + string? GetMaxText(); +} diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs index 9f8f928061..9132cc2f7b 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Content.Shared._DEN.Requirements.Managers; using Content.Shared.CCVar; using Content.Shared.Localizations; @@ -12,17 +13,19 @@ namespace Content.Shared._DEN.Requirements.PlayerRequirements; /// An abstract class for playtime requirements that expect a playtime to be within /// optional minimum and maximum parameters. /// -public abstract partial class PlayerPlaytimeRequirement : PlayerRequirement +public abstract partial class PlayerPlaytimeRequirement : PlayerRequirement, IPlayerRangeRequirement { /// /// The minimum time you can have in this tracker. /// - [DataField] public TimeSpan? MinTime = null; + [DataField("minTime")] + public TimeSpan? Min { get; set; } = null; /// /// The maximum time you can have in this tracker. /// - [DataField] public TimeSpan? MaxTime = null; + [DataField("maxTime")] + public TimeSpan? Max { get; set; } = null; /// public override bool PreCheck(PlayerRequirementContext context) @@ -33,22 +36,6 @@ public override bool PreCheck(PlayerRequirementContext context) return ShouldAutoPass() || context.Playtimes != null; } - /// - /// Check if a given playtime tracker fits within the minimum and maximum times of this requirement. - /// - /// The playtime to check. - /// Whether or not this playtime is valid. - protected bool IsValidPlaytime(TimeSpan playtime) - { - if (MinTime != null & playtime < MinTime) - return false; - - if (MaxTime != null & playtime > MaxTime) - return false; - - return true; - } - /// /// Whether or not this requirement should auto-pass. This applies if role timers /// are disabled, because playtimes shouldn't matter anyway in this case - we shouldn't @@ -64,45 +51,58 @@ protected static bool ShouldAutoPass() } /// - /// Gets a localized "reason" string for this requirement's playtime ranges. + /// Format a playtime TimeSpan into text to display to the player. /// - /// - /// For example: "Less than 30 minutes", "At least 120 minutes", "Between 20 minutes and 180 minutes". - /// - /// - /// A string describing how much playtime you should have. Null if both minimum and maximum as null. - /// - protected string? GetPlaytimeConstraintReason() + /// The playtime to format. + /// The formatted playtime, if playtime is not null. + private static string? FormatPlaytime(TimeSpan? playtime) { - if (MinTime == null && MaxTime == null) + if (playtime is null) return null; - var minTimeString = FormatPlaytime(MinTime); - var maxTimeString = FormatPlaytime(MaxTime); + var playtimeString = ContentLocalizationManager.FormatPlaytime(playtime.Value); + return Loc.GetString("player-requirement-format-time", + ("playtime", playtimeString)); + } - return (minTimeString, maxTimeString) switch - { - (not null, not null) => Loc.GetString("player-requirement-playtime-minmax-time", - ("minimum", minTimeString), ("maximum", maxTimeString)), + /// + public string? GetMinText() + { + return FormatPlaytime(Min); + } - (null, not null) => Loc.GetString("player-requirement-playtime-maximum-time", - ("playtime", maxTimeString)), + /// + public string? GetMaxText() + { + return FormatPlaytime(Max); + } - (not null, null) => Loc.GetString("player-requirement-playtime-minimum-time", - ("playtime", minTimeString)), + /// + /// Check if the given playtime is in range. + /// + /// The playtime to check. + /// Whether or not the playtime is in range. + protected bool IsInRange(TimeSpan playtime) + { + if (this is IPlayerRangeRequirement range) + return range.IsInRange(playtime); - _ => null - }; + return false; } - private static string? FormatPlaytime(TimeSpan? playtime) + /// + /// Get the text to display to the player that represents the range of valid playtimes. + /// + /// The playtime range description. + /// Whether or not this operation was successful. + protected bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString) { - if (playtime is null) - return null; + playtimeString = null; - var playtimeString = ContentLocalizationManager.FormatPlaytime(playtime.Value); - return Loc.GetString("player-requirement-format-time", - ("playtime", playtimeString)); + if (this is IPlayerRangeRequirement range) + playtimeString = range.GetRangeConstraintReason(); + + return playtimeString != null; } } @@ -128,7 +128,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) if (playtime is null) return false; - return IsValidPlaytime(playtime.Value); + return IsInRange(playtime.Value); } /// @@ -143,8 +143,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) var deptName = FormatDepartment(protoMan); // Get the playtime constraint string. - var playtimeString = GetPlaytimeConstraintReason(); - if (playtimeString == null) + if (!TryGetRangeConstraintReason(out var playtimeString)) return null; var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason", @@ -228,7 +227,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) if (playtime is null) return false; - return IsValidPlaytime(playtime.Value); + return IsInRange(playtime.Value); } /// @@ -243,8 +242,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) var jobName = FormatJob(protoMan); // Get the playtime constraint string. - var playtimeString = GetPlaytimeConstraintReason(); - if (playtimeString == null) + if (!TryGetRangeConstraintReason(out var playtimeString)) return null; var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason", diff --git a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl index 63b2c095c6..2a88288b75 100644 --- a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl @@ -1,6 +1,7 @@ +player-requirement-range-minimum-reason = at least {$minimum} +player-requirement-range-maximum-reason = at most {$maximum} +player-requirement-range-minmax-reason = between {$minimum} and {$maximum} + count-requirement-constant-reason = exactly {$count} -count-requirement-range-minimum-reason = at least {$minimum} -count-requirement-range-maximum-reason = at most {$maximum} -count-requirement-range-minmax-reason = between {$minimum} and {$maximum} count-requirement-any-reason = any count-requirement-all-reason = all From e2b0805f4fa77f607a00f89b199dfed87e75b37e Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:28:10 -0500 Subject: [PATCH 29/54] add age requirement --- .../PlayerRequirements.Profile.cs | 122 ++++++++++++++---- .../requirements/profile-requirements.ftl | 5 + 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs index 2fbfd17e05..f0aad45a13 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared._DEN.Requirements.Managers; using Content.Shared._DEN.Traits.Prototypes; @@ -7,18 +8,21 @@ namespace Content.Shared._DEN.Requirements.PlayerRequirements; /// -/// Checks if a player's character has a required number of the given traits. +/// Checks if a player's character is within the given age range. /// -public sealed partial class PlayerTraitRequirement : PlayerRequirement +public sealed partial class PlayerAgeRequirement : PlayerRequirement, IPlayerRangeRequirement { /// - /// Traits that the character needs to have to the pass the requirement. + /// Minimum age of the character to pass the requirement. /// [DataField] - public HashSet> Traits = new(); + public int? Min { get; set; } = null; + /// + /// Maximum age of the character to pass the requirement. + /// [DataField] - public CountRequirement Count; + public int? Max { get; set; } = null; /// public override bool PreCheck(PlayerRequirementContext context) @@ -32,38 +36,49 @@ public override bool CheckRequirement(PlayerRequirementContext context) if (context.Profile == null) return false; - var profileTraits = context.Profile.EntityTraitPreferences; - return Count.CheckRequirement(profileTraits, Traits); + if (this is IPlayerRangeRequirement range) + return range.IsInRange(context.Profile.Age); + + return false; } /// public override string? GetReason() { - var protoMan = IoCManager.Resolve(); - var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan)); - var traitList = string.Join(", ", traitNames); - var constraintReason = Count.GetReason(); + if (!TryGetRangeConstraintReason(out var constraintReason)) + return null; - return Loc.GetString("player-requirement-trait-reason", + // "Must be between 20 and 40 years old." + return Loc.GetString("player-requirement-age-reason", ("inverted", Inverted), - ("constraint", constraintReason), - ("traits", traitList)); + ("constraint", constraintReason)); } /// - /// Localizes a trait ID into a formatted trait name. + /// Get the text to display to the player that represents the range of valid playtimes. /// - /// The ID of the trait. - /// The prototype manager. - /// A formatted trait name string. - private static string LocalizeTrait(ProtoId traitId, IPrototypeManager protoMan) + /// The playtime range description. + /// Whether or not this operation was successful. + private bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? reason) { - var traitName = traitId; + reason = null; - if (protoMan.TryIndex(traitId, out var trait)) - traitName = Loc.GetString(trait.Name); + if (this is IPlayerRangeRequirement range) + reason = range.GetRangeConstraintReason(); - return Loc.GetString("player-requirement-format-trait", ("trait", traitName)); + return reason != null; + } + + /// + public string? GetMinText() + { + return Min?.ToString(); + } + + /// + public string? GetMaxText() + { + return Max?.ToString(); } } @@ -127,3 +142,64 @@ private static string LocalizeSpecies(ProtoId speciesId, IProt return Loc.GetString("player-requirement-format-species", ("species", speciesName)); } } + +/// +/// Checks if a player's character has a required number of the given traits. +/// +public sealed partial class PlayerTraitRequirement : PlayerRequirement +{ + /// + /// Traits that the character needs to have to the pass the requirement. + /// + [DataField] + public HashSet> Traits = new(); + + [DataField] + public CountRequirement Count; + + /// + public override bool PreCheck(PlayerRequirementContext context) + { + return context.Profile != null; + } + + /// + public override bool CheckRequirement(PlayerRequirementContext context) + { + if (context.Profile == null) + return false; + + var profileTraits = context.Profile.EntityTraitPreferences; + return Count.CheckRequirement(profileTraits, Traits); + } + + /// + public override string? GetReason() + { + var protoMan = IoCManager.Resolve(); + var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan)); + var traitList = string.Join(", ", traitNames); + var constraintReason = Count.GetReason(); + + return Loc.GetString("player-requirement-trait-reason", + ("inverted", Inverted), + ("constraint", constraintReason), + ("traits", traitList)); + } + + /// + /// Localizes a trait ID into a formatted trait name. + /// + /// The ID of the trait. + /// The prototype manager. + /// A formatted trait name string. + private static string LocalizeTrait(ProtoId traitId, IPrototypeManager protoMan) + { + var traitName = traitId; + + if (protoMan.TryIndex(traitId, out var trait)) + traitName = Loc.GetString(trait.Name); + + return Loc.GetString("player-requirement-format-trait", ("trait", traitName)); + } +} diff --git a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl index 02bce0b096..f2799cb22c 100644 --- a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl @@ -7,3 +7,8 @@ player-requirement-species-reason = Must{$inverted -> [true] {" "}not *[false] {""} } be one of these species: {$species} + +player-requirement-age-reason = Must{$inverted -> + [true] {" "}not + *[false] {""} +} be {$constraint} years old. From 5a9e35d9000a19717dcf8044c68eb91054a8c852 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:04:23 -0500 Subject: [PATCH 30/54] add overall playtime requirement --- .../PlayerRequirements/IPlayerRequirement.cs | 1 - .../PlayerRequirements.Playtime.cs | 60 +++++++++++++++++++ .../requirements/playtime-requirements.ftl | 1 + 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs index 5180944eea..a1cc4bb623 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs @@ -1,4 +1,3 @@ -using System.Numerics; using Content.Shared._DEN.Requirements.Managers; namespace Content.Shared._DEN.Requirements.PlayerRequirements; diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs index 9132cc2f7b..3cdc3ee4ac 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -2,6 +2,7 @@ using Content.Shared._DEN.Requirements.Managers; using Content.Shared.CCVar; using Content.Shared.Localizations; +using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Roles; using Content.Shared.Roles.Jobs; using Robust.Shared.Configuration; @@ -300,3 +301,62 @@ private string FormatJob(IPrototypeManager protoMan) return playtime; } } + +/// +/// Checks if a player's total overall playtime fits within a given playtime range. +/// +public sealed partial class PlayerOverallPlaytimeRequirement : PlayerPlaytimeRequirement +{ + /// + public override bool CheckRequirement(PlayerRequirementContext context) + { + // Auto-pass if role timers are disabled. + if (ShouldAutoPass()) + return true; + + var playtime = GetOverallPlaytime(context); + if (playtime is null) + return false; + + return IsInRange(playtime.Value); + } + + /// + /// Get the overall playtime for this context. + /// + /// The context being used for checking this requirement. + /// The player's overall playtime. + private static TimeSpan? GetOverallPlaytime(PlayerRequirementContext context) + { + var overallTracker = PlayTimeTrackingShared.TrackerOverall; + var playtime = TimeSpan.Zero; + + if (context.Playtimes == null) + return null; + + if (context.Playtimes.TryGetValue(overallTracker, out var tracker)) + playtime = tracker; + + return playtime; + } + + /// + public override string? GetReason() + { + // Do not give a reason if role timers are disabled. + if (ShouldAutoPass()) + return null; + + // Get the playtime constraint string. + if (!TryGetRangeConstraintReason(out var playtimeString)) + return null; + + var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason", + ("inverted", Inverted), + ("timeConstraint", playtimeString)); + + // E.g. "You must have 300h of playtime overall." + return Loc.GetString("player-requirement-overall-playtime-reason", + ("constraint", constraintReason)); + } +} diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl index d5516658ae..64bbd8ba84 100644 --- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl @@ -8,3 +8,4 @@ player-requirement-playtime-constraint-reason = Must{$inverted -> player-requirement-department-playtime-reason = {$constraint} in the {$department} department. player-requirement-job-playtime-reason = {$constraint} as a {$job}. +player-requirement-overall-playtime-reason = {$constraint} overall. From 23d9543d5a4aa937bfcec8b6cea70b0f14d72d65 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:51:50 -0500 Subject: [PATCH 31/54] obsolete more job requirements --- Content.Shared/Roles/JobRequirement/AgeRequirement.cs | 1 + .../Roles/JobRequirement/DepartmentTimeRequirement.cs | 1 + .../Roles/JobRequirement/OverallPlaytimeRequirement.cs | 1 + Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs | 1 + Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs | 1 + Content.Shared/Roles/JobRequirement/TraitsRequirement.cs | 2 +- .../_DEN/Roles/JobRequirement/EntityTraitsRequirement.cs | 1 + 7 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Content.Shared/Roles/JobRequirement/AgeRequirement.cs b/Content.Shared/Roles/JobRequirement/AgeRequirement.cs index 30f607adf7..a7106484df 100644 --- a/Content.Shared/Roles/JobRequirement/AgeRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/AgeRequirement.cs @@ -12,6 +12,7 @@ namespace Content.Shared.Roles; /// [UsedImplicitly] [Serializable, NetSerializable] +[Obsolete("Use PlayerAgeRequirement instead")] // DEN public sealed partial class AgeRequirement : JobRequirement { [DataField(required: true)] diff --git a/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs b/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs index 4034b8b419..a05cb952f9 100644 --- a/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs @@ -10,6 +10,7 @@ namespace Content.Shared.Roles; [UsedImplicitly] [Serializable, NetSerializable] +[Obsolete("Use PlayerDepartmentPlaytimeRequirement instead")] // DEN public sealed partial class DepartmentTimeRequirement : JobRequirement { /// diff --git a/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs b/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs index 67b3938e1a..543f16b3b9 100644 --- a/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs @@ -11,6 +11,7 @@ namespace Content.Shared.Roles; [UsedImplicitly] [Serializable, NetSerializable] +[Obsolete("Use PlayerOverallPlaytimeRequirement instead")] // DEN public sealed partial class OverallPlaytimeRequirement : JobRequirement { /// diff --git a/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs b/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs index f096cfcb42..c5f6fce5ec 100644 --- a/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs @@ -12,6 +12,7 @@ namespace Content.Shared.Roles; [UsedImplicitly] [Serializable, NetSerializable] +[Obsolete("Use PlayerJobPlaytimeRequirement instead")] // DEN public sealed partial class RoleTimeRequirement : JobRequirement { /// diff --git a/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs b/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs index 68c069931f..736247c5fa 100644 --- a/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs @@ -14,6 +14,7 @@ namespace Content.Shared.Roles; /// [UsedImplicitly] [Serializable, NetSerializable] +[Obsolete("Use PlayerSpeciesRequirement instead")] // DEN public sealed partial class SpeciesRequirement : JobRequirement { [DataField(required: true)] diff --git a/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs b/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs index 3ebba44cf2..a1016c107e 100644 --- a/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs @@ -15,7 +15,7 @@ namespace Content.Shared.Roles; /// [UsedImplicitly] [Serializable, NetSerializable] -[Obsolete("Use EntityTraitsRequirement instead")] // DEN +[Obsolete("Use PlayerTraitRequirement instead")] // DEN public sealed partial class TraitsRequirement : JobRequirement { [DataField(required: true)] diff --git a/Content.Shared/_DEN/Roles/JobRequirement/EntityTraitsRequirement.cs b/Content.Shared/_DEN/Roles/JobRequirement/EntityTraitsRequirement.cs index ad813ecd44..965db85379 100644 --- a/Content.Shared/_DEN/Roles/JobRequirement/EntityTraitsRequirement.cs +++ b/Content.Shared/_DEN/Roles/JobRequirement/EntityTraitsRequirement.cs @@ -15,6 +15,7 @@ namespace Content.Shared._DEN.Roles; /// [UsedImplicitly] [Serializable, NetSerializable] +[Obsolete("Use PlayerTraitRequirement instead")] public sealed partial class EntityTraitsRequirement : JobRequirement { [DataField(required: true)] From df0bb0a3da2d826726d6019c784c727ddac72c0a Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:53:14 -0500 Subject: [PATCH 32/54] move this out of IPlayerRequirementManager --- .../Managers/IPlayerRequirementManager.cs | 17 ---------------- .../SharedPlayerRequirementManager.cs | 20 +++++++++++++++---- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs index 297d3b03f6..e0ac562b06 100644 --- a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs +++ b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs @@ -9,23 +9,6 @@ namespace Content.Shared._DEN.Requirements.Managers; /// public interface IPlayerRequirementManager { - /// - /// Check a context against enumerable requirements and gets the final pass/fail status of these requirements. - /// - /// The context containing fields to check against the requirements. - /// An enumerable collection of requirements. - /// Whether or not this context passes *all* requirements. If even one fails, then this is false. - bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements); - - /// - /// Whether or not the item associated with a set of requirements should be hidden - /// from the player, such as in UI. - /// - /// The context containing fields to check against the requirements. - /// An enumerable collection of requirements. - /// Whether or not the item should be hidden from the player. - bool ShouldHide(PlayerRequirementContext context, IEnumerable requirements); - /// /// Creates a new PlayerRequirementContext with context fields pre-filled. /// diff --git a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs index 3205637ae7..adbfef012f 100644 --- a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs +++ b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs @@ -10,9 +10,14 @@ namespace Content.Shared._DEN.Requirements.Managers; /// public abstract partial class SharedPlayerRequirementManager : IPlayerRequirementManager { - /// + /// + /// Check a context against enumerable requirements and gets the final pass/fail status of these requirements. + /// + /// The context containing fields to check against the requirements. + /// An enumerable collection of requirements. + /// Whether or not this context passes *all* requirements. If even one fails, then this is false. [PublicAPI] - public bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements) + public static bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements) { foreach (var requirement in requirements) if (!CheckRequirement(context, requirement)) @@ -21,9 +26,16 @@ public bool CheckRequirements(PlayerRequirementContext context, IEnumerable + /// + /// Whether or not the item associated with a set of requirements should be hidden + /// from the player, such as in UI. + /// + /// The context containing fields to check against the requirements. + /// An enumerable collection of requirements. + /// Whether or not the item should be hidden from the player. + [PublicAPI] - public bool ShouldHide(PlayerRequirementContext context, IEnumerable requirements) + public static bool ShouldHide(PlayerRequirementContext context, IEnumerable requirements) { foreach (var requirement in requirements) if (!CheckRequirement(context, requirement) && requirement.HideIfFailed) From ac4282f977921349c1517bff4f42e8b1f927976a Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:54:52 -0500 Subject: [PATCH 33/54] use static CheckRequirements --- Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs index ae503dc1d4..b1281a40d1 100644 --- a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs +++ b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs @@ -1,7 +1,6 @@ using Content.Shared._DEN.Requirements.Managers; using Content.Shared._DEN.Traits.EntitySystems; using Content.Shared.GameTicking; -using Content.Shared.Humanoid; using Content.Shared.Roles; using Robust.Shared.Prototypes; @@ -46,7 +45,7 @@ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args) var context = _requirements.GetPlayerContext(args.Player); context.Profile = args.Profile; - if (!_requirements.CheckRequirements(context, trait.Requirements)) + if (!SharedPlayerRequirementManager.CheckRequirements(context, trait.Requirements)) { Log.Error($"Tried to spawn trait {traitId} on {ToPrettyString(mob)}, but we failed the requirements to do so!"); continue; From 1dab83d66f1e845e1ace9bef533a4fc152fac805 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:06:51 -0500 Subject: [PATCH 34/54] add PlayerRequirementLoadoutEffect --- .../Effects/JobRequirementLoadoutEffect.cs | 1 + .../Effects/PlayerRequirementLoadoutEffect.cs | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs diff --git a/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs b/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs index b9bf4b38bc..e793cd7e85 100644 --- a/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs +++ b/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs @@ -12,6 +12,7 @@ namespace Content.Shared.Preferences.Loadouts.Effects; /// /// Checks for a job requirement to be met such as playtime. /// +[Obsolete("Use PlayerRequirementLoadoutEffect")] // DEN public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect { [DataField(required: true)] diff --git a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs new file mode 100644 index 0000000000..c3f75fa0c0 --- /dev/null +++ b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs @@ -0,0 +1,47 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared._DEN.Requirements.Managers; +using Content.Shared._DEN.Requirements.PlayerRequirements; +using Content.Shared.Preferences; +using Content.Shared.Preferences.Loadouts; +using Content.Shared.Preferences.Loadouts.Effects; +using Robust.Shared.Player; +using Robust.Shared.Utility; + +namespace Content.Shared._DEN.Preferences.Loadouts.Effects; + +/// +/// Checks for a player requirement to be met. +/// +public sealed partial class PlayerRequirementLoadoutEffect : LoadoutEffect +{ + [DataField(required: true)] + public IPlayerRequirement Requirement = default!; + + public override bool Validate(HumanoidCharacterProfile profile, + RoleLoadout loadout, + ICommonSession? session, + IDependencyCollection collection, + [NotNullWhen(false)] out FormattedMessage? reason) + { + if (session == null) + { + reason = FormattedMessage.Empty; + return false; + } + + var requirements = collection.Resolve(); + var context = requirements.GetPlayerContext(session); + context.Profile = profile; + var success = SharedPlayerRequirementManager.CheckRequirement(context, Requirement); + + if (!success) + { + var reasonText = Requirement.GetReason() ?? string.Empty; + reason = FormattedMessage.FromMarkupPermissive(reasonText); + return false; + } + + reason = null; + return true; + } +} From 63f43f892bb0f3e52891570f60dfb735dd3ecc49 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:23:03 -0500 Subject: [PATCH 35/54] static ShouldHide --- Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs index 2d9c28351b..ba49b0bb79 100644 --- a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs +++ b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs @@ -67,7 +67,7 @@ private void UpdateVisibility(EntityTraitPrototype trait) return; var context = _requirements.GetPlayerContext(session); - Visible = !_requirements.ShouldHide(context, trait.Requirements); + Visible = !SharedPlayerRequirementManager.ShouldHide(context, trait.Requirements); } /// From 8a640ccd5d832b1c4aa0f85f91df62f26edbb8e9 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:23:59 -0500 Subject: [PATCH 36/54] oops i should disable timers here --- .../Loadouts/Effects/PlayerRequirementLoadoutEffect.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs index c3f75fa0c0..4c2d22c9ce 100644 --- a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs +++ b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs @@ -1,9 +1,11 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared._DEN.Requirements.Managers; using Content.Shared._DEN.Requirements.PlayerRequirements; +using Content.Shared.CCVar; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Preferences.Loadouts.Effects; +using Robust.Shared.Configuration; using Robust.Shared.Player; using Robust.Shared.Utility; @@ -23,7 +25,10 @@ public override bool Validate(HumanoidCharacterProfile profile, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason) { - if (session == null) + var configurationManager = collection.Resolve(); + var timersDisabled = !configurationManager.GetCVar(CCVars.GameRoleLoadoutTimers); + + if (session == null || timersDisabled) { reason = FormattedMessage.Empty; return false; From f5822a82290aa433565374f58aae76f9486225c0 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:48:58 -0500 Subject: [PATCH 37/54] add playtime requirement types --- .../Effects/PlayerRequirementLoadoutEffect.cs | 9 +++---- .../PlayerRequirements.Playtime.cs | 25 +++++++++++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs index 4c2d22c9ce..269e323c67 100644 --- a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs +++ b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs @@ -1,11 +1,9 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared._DEN.Requirements.Managers; using Content.Shared._DEN.Requirements.PlayerRequirements; -using Content.Shared.CCVar; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Preferences.Loadouts.Effects; -using Robust.Shared.Configuration; using Robust.Shared.Player; using Robust.Shared.Utility; @@ -25,10 +23,9 @@ public override bool Validate(HumanoidCharacterProfile profile, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason) { - var configurationManager = collection.Resolve(); - var timersDisabled = !configurationManager.GetCVar(CCVars.GameRoleLoadoutTimers); - - if (session == null || timersDisabled) + if (session == null + // Auto-pass playtime requirements if they're disabled + || Requirement is PlayerPlaytimeRequirement playtimeReq && playtimeReq.ShouldAutoPass()) { reason = FormattedMessage.Empty; return false; diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs index 3cdc3ee4ac..23baf5403a 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -28,6 +28,13 @@ public abstract partial class PlayerPlaytimeRequirement : PlayerRequirement, IPl [DataField("maxTime")] public TimeSpan? Max { get; set; } = null; + /// + /// The "type" of playtime requirement this is. + /// This affects what CVAR is used to turn the timer off. + /// + [DataField] + public PlaytimeRequirementType RequirementType = PlaytimeRequirementType.Role; + /// public override bool PreCheck(PlayerRequirementContext context) { @@ -45,10 +52,17 @@ public override bool PreCheck(PlayerRequirementContext context) /// /// Whether or not this requirement should auto-pass. /// - protected static bool ShouldAutoPass() + public bool ShouldAutoPass() { var config = IoCManager.Resolve(); - return !config.GetCVar(CCVars.GameRoleTimers); + var timerEnabled = RequirementType switch + { + PlaytimeRequirementType.Role => config.GetCVar(CCVars.GameRoleTimers), + PlaytimeRequirementType.Loadout => config.GetCVar(CCVars.GameRoleLoadoutTimers), + _ => throw new ArgumentOutOfRangeException(nameof(RequirementType)), + }; + + return !timerEnabled; } /// @@ -107,6 +121,13 @@ protected bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playt } } +[Serializable] +public enum PlaytimeRequirementType +{ + Role, + Loadout +} + /// /// Checks if a player's total playtime in a given department fits within a given playtime range. /// From 33c61bf654dabdebe132cfc8e8048780bd05215d Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:53:37 -0500 Subject: [PATCH 38/54] use player requirements in loadouts --- .../Loadouts/Jobs/Cargo/cargo_technician.yml | 45 +++++++---- .../Loadouts/Jobs/Cargo/quartermaster.yml | 17 ++-- .../Loadouts/Jobs/Civilian/bartender.yml | 17 ++-- .../Loadouts/Jobs/Civilian/chaplain.yml | 53 +++++++++---- .../Loadouts/Jobs/Civilian/janitor.yml | 17 ++-- .../Loadouts/Jobs/Civilian/passenger.yml | 34 +++++--- .../Loadouts/Jobs/Command/captain.yml | 17 ++-- .../Jobs/Command/head_of_personnel.yml | 34 +++++--- .../Jobs/Engineering/chief_engineer.yml | 17 ++-- .../Jobs/Engineering/station_engineer.yml | 45 +++++++---- .../Loadouts/Jobs/Medical/role_timers.yml | 79 +++++++++++++------ .../Jobs/Science/research_director.yml | 17 ++-- .../Loadouts/Jobs/Science/scientist.yml | 15 +++- .../Jobs/Security/head_of_security.yml | 17 ++-- .../Jobs/Security/security_officer.yml | 30 ++++--- .../Loadouts/Jobs/Wildcards/reporter.yml | 34 +++++--- .../Loadouts/Miscellaneous/glasses.yml | 34 +++++--- .../Loadouts/Miscellaneous/jobtrinkets.yml | 34 +++++--- .../Loadouts/Miscellaneous/trinkets.yml | 76 ++++++++++++------ 19 files changed, 443 insertions(+), 189 deletions(-) diff --git a/Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml b/Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml index d23027130f..069dceded8 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml @@ -2,21 +2,38 @@ - type: loadoutEffectGroup id: SeniorCargo effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobCargoTechnician - time: 21600 #6 hrs - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobSalvageSpecialist - time: 21600 #6 hrs - - !type:JobRequirementLoadoutEffect - requirement: - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: CargoTechnician + minTime: 6h + requirementType: Loadout + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: SalvageSpecialist + minTime: 6h + requirementType: Loadout + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerDepartmentPlaytimeRequirement department: Cargo - time: 216000 # 60 hrs + minTime: 60h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobCargoTechnician + # time: 21600 #6 hrs + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobSalvageSpecialist + # time: 21600 #6 hrs + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:DepartmentTimeRequirement + # department: Cargo + # time: 216000 # 60 hrs + # End DEN # Head - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml index 602b2d36fd..1b7c4bf524 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml @@ -2,11 +2,18 @@ - type: loadoutEffectGroup id: MasterQM effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobQuartermaster - time: 20h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: QuarterMaster + minTime: 20h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobQuartermaster + # time: 20h + # End DEN # Jumpsuit - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml index b8a8744915..b928dd64e1 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml @@ -1,11 +1,18 @@ - type: loadoutEffectGroup id: SeniorBar effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobBartender - time: 52h # 1 hour per week for 1 year + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Bartender + minTime: 52h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobBartender + # time: 52h # 1 hour per week for 1 year + # End DEN # Head - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml index 0bfc99d41f..1ab03e734a 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml @@ -2,31 +2,52 @@ - type: loadoutEffectGroup id: NanoTrasenBibleRequirement effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobCaptain - time: 7200 #2 hrs + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Captain + minTime: 2h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobCaptain + # time: 7200 #2 hrs + # End DEN # Playtime requirement for Druid Bible, Druidic Tablet - type: loadoutEffectGroup id: DruidBibleRequirement effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobBotanist - time: 18000 #5 hrs + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Botanist + minTime: 5h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobBotanist + # time: 18000 #5 hrs + # End DEN # Playtime requirement for Clown Bible, Mirth of the Honkmother - type: loadoutEffectGroup id: ClownBibleRequirement effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobClown - time: 18000 #5 hrs + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Clown + minTime: 5h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobClown + # time: 18000 #5 hrs + # End DEN # Head - type: loadout @@ -132,7 +153,7 @@ storage: back: - BibleNanoTrasen - + - type: loadout id: BibleNarsie storage: diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml index 431b83a99d..02df8d61ec 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml @@ -1,11 +1,18 @@ - type: loadoutEffectGroup id: SeniorJanitorial effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobJanitor - time: 52h # 1 hour per week for 1 year + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Janitor + minTime: 52h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobJanitor + # time: 52h # 1 hour per week for 1 year + # End DEN # Head - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml index 1ed5e8aca0..c9ac6729ee 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml @@ -2,21 +2,35 @@ - type: loadoutEffectGroup id: GreyTider effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobPassenger - time: 10h # silly reward for people who play passenger a lot + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Passenger + minTime: 10h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobPassenger + # time: 10h # silly reward for people who play passenger a lot + # End DEN # Head of Greytide (for grey mantle) - type: loadoutEffectGroup id: MasterGrey effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobPassenger - time: 20h # fun mantle for the most experienced greytiders + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Passenger + minTime: 20h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobPassenger + # time: 20h # fun mantle for the most experienced greytiders + # End DEN # Face - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Command/captain.yml b/Resources/Prototypes/Loadouts/Jobs/Command/captain.yml index 2951678968..62b4920f18 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Command/captain.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Command/captain.yml @@ -2,11 +2,18 @@ - type: loadoutEffectGroup id: MasterCap effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobCaptain - time: 20h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Captain + minTime: 20h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobCaptain + # time: 20h + # End DEN # Jumpsuit - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml b/Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml index 17a84b7386..3bc5952afa 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml @@ -2,21 +2,35 @@ - type: loadoutEffectGroup id: MasterHoP effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobHeadOfPersonnel - time: 20h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: HeadOfPersonnel + minTime: 20h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobHeadOfPersonnel + # time: 20h + # End DEN # Professional HoP Time - type: loadoutEffectGroup id: ProfessionalHoP effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobHeadOfPersonnel - time: 15h # special reward for HoP mains + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: HeadOfPersonnel + minTime: 15h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobHeadOfPersonnel + # time: 15h # special reward for HoP mains + # End DEN # Jumpsuit - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml b/Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml index 55f184a168..d2d313d627 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml @@ -2,11 +2,18 @@ - type: loadoutEffectGroup id: MasterCE effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobChiefEngineer - time: 20h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: ChiefEngineer + minTime: 20h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobChiefEngineer + # time: 20h + # End DEN # Jumpsuit - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml index 64bfe79a4d..9c018b2725 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml @@ -2,21 +2,38 @@ - type: loadoutEffectGroup id: SeniorEngineering effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobAtmosphericTechnician - time: 6h - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobStationEngineer - time: 6h - - !type:JobRequirementLoadoutEffect - requirement: - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: AtmosphericsTechnician + minTime: 6h + requirementType: Loadout + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: StationEngineer + minTime: 6h + requirementType: Loadout + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerDepartmentPlaytimeRequirement department: Engineering - time: 60h + minTime: 60h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobAtmosphericTechnician + # time: 6h + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobStationEngineer + # time: 6h + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:DepartmentTimeRequirement + # department: Engineering + # time: 60h + # End DEN # Head - type: startingGear diff --git a/Resources/Prototypes/Loadouts/Jobs/Medical/role_timers.yml b/Resources/Prototypes/Loadouts/Jobs/Medical/role_timers.yml index f023cfc227..cc5eadd03f 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Medical/role_timers.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Medical/role_timers.yml @@ -2,38 +2,69 @@ - type: loadoutEffectGroup id: SeniorPhysician effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobChemist - time: 6h - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobMedicalDoctor - time: 6h - - !type:JobRequirementLoadoutEffect - requirement: - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Chemist + minTime: 6h + requirementType: Loadout + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: MedicalDoctor + minTime: 6h + requirementType: Loadout + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerDepartmentPlaytimeRequirement department: Medical - time: 60h + minTime: 60h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobChemist + # time: 6h + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobMedicalDoctor + # time: 6h + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:DepartmentTimeRequirement + # department: Medical + # time: 60h + # End DEN # Mid-level timer for players who have a solid amount of experience - type: loadoutEffectGroup id: MedicalJourneymanTimer effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobMedicalDoctor - time: 20h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: MedicalDoctor + minTime: 20h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobMedicalDoctor + # time: 20h + # End DEN # Mid-level timer for CMO - type: loadoutEffectGroup id: MasterCMO effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobChiefMedicalOfficer - time: 20h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: ChiefMedicalOfficer + minTime: 20h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobChiefMedicalOfficer + # time: 20h + # End DEN diff --git a/Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml b/Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml index 3717c1c67c..a325320db0 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml @@ -2,11 +2,18 @@ - type: loadoutEffectGroup id: MasterRD effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobResearchDirector - time: 20h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: ResearchDirector + minTime: 20h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobResearchDirector + # time: 20h + # End DEN # Head - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml b/Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml index 080015f0da..9b1591f9d9 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml @@ -2,11 +2,18 @@ - type: loadoutEffectGroup id: SeniorResearcher effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerDepartmentPlaytimeRequirement department: Science - time: 60h + minTime: 60h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:DepartmentTimeRequirement + # department: Science + # time: 60h + # End DEN # Head - type: startingGear diff --git a/Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml b/Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml index e85e1c8ccb..89c8694875 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml @@ -2,11 +2,18 @@ - type: loadoutEffectGroup id: MasterHoS effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobHeadOfSecurity - time: 20h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: HeadOfSecurity + minTime: 20h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobHeadOfSecurity + # time: 20h + # End DEN # Jumpsuit - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml b/Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml index e0b0ce1a3f..6e2c90e9ab 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml @@ -2,16 +2,28 @@ - type: loadoutEffectGroup id: SeniorOfficer effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobWarden - time: 6h - - !type:JobRequirementLoadoutEffect - requirement: - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Warden + minTime: 6h + requirementType: Loadout + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerDepartmentPlaytimeRequirement department: Security - time: 60h + minTime: 60h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobWarden + # time: 6h + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:DepartmentTimeRequirement + # department: Security + # time: 60h + # End DEN # Head - type: loadout diff --git a/Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml b/Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml index f5f9825526..85f08d0a61 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml @@ -13,11 +13,18 @@ - type: loadout id: ReporterPressFedora effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobReporter - time: 10h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Reporter + minTime: 10h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobReporter + # time: 10h + # End DEN equipment: head: ClothingHeadHatFedoraPress @@ -25,10 +32,17 @@ - type: loadout id: ReporterPressVest effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobReporter - time: 35h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Reporter + minTime: 35h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobReporter + # time: 35h + # End DEN equipment: outerClothing: ClothingOuterVestPress diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml b/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml index 8a474d359a..ed80248fb7 100644 --- a/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml +++ b/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml @@ -2,20 +2,34 @@ - type: loadoutEffectGroup id: JamjarTimer effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobLibrarian - time: 1h # for being the biggest nerd on the station + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Librarian + minTime: 1h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobLibrarian + # time: 1h # for being the biggest nerd on the station + # End DEN - type: loadoutEffectGroup id: JensenTimer effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:DepartmentTimeRequirement - department: Cargo - time: 10h # 10 hours of being a space trucker + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Cargo + minTime: 10h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:DepartmentTimeRequirement + # department: Cargo + # time: 10h # 10 hours of being a space trucker + # End DEN # Basic options # Glasses diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml b/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml index 27d266c5bb..173cb7d087 100644 --- a/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml +++ b/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml @@ -12,11 +12,18 @@ - type: loadout id: FlowerWaterClown effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobClown - time: 4h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Clown + minTime: 4h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobClown + # time: 4h + # End DEN storage: back: - SprayFlowerPin @@ -24,11 +31,18 @@ - type: loadout id: SecStar effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:DepartmentTimeRequirement - department: Security - time: 100h + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerDepartmentPlaytimeRequirement + department: Security + minTime: 100h + requirementType: Loadout + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:DepartmentTimeRequirement + # department: Security + # time: 100h + # End DEN storage: back: - Dinkystar diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml b/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml index 50331ff89d..ccb084300f 100644 --- a/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml +++ b/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml @@ -2,11 +2,17 @@ - type: loadoutEffectGroup id: Command effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerDepartmentPlaytimeRequirement department: Command - time: 1h + minTime: 1h + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:DepartmentTimeRequirement + # department: Command + # time: 1h + # End DEN # Flowers - type: loadout @@ -277,11 +283,17 @@ - type: loadout id: OffsetCaneClown effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobClown - time: 3600 # 1hr + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Clown + minTime: 1h + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobClown + # time: 3600 # 1hr + # End DEN storage: back: - OffsetCaneClown @@ -290,11 +302,17 @@ - type: loadout id: OffsetCaneMime effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobMime - time: 3600 # 1hr + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Mime + minTime: 1h + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobMime + # time: 3600 # 1hr + # End DEN storage: back: - OffsetCaneMime @@ -303,11 +321,17 @@ - type: loadout id: OffsetCaneNT effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerDepartmentPlaytimeRequirement department: Command - time: 18000 # 5hr + minTime: 5h + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:DepartmentTimeRequirement + # department: Command + # time: 18000 # 5hr + # End DEN storage: back: - OffsetCaneNT @@ -316,11 +340,17 @@ - type: loadout id: Cane effects: - - !type:JobRequirementLoadoutEffect - requirement: - !type:RoleTimeRequirement - role: JobLibrarian - time: 3600 # 1hr same as Jamjar. + # Begin DEN: Use PlayerRequirements + - !type:PlayerRequirementLoadoutEffect + requirement: !type:PlayerJobPlaytimeRequirement + job: Librarian + minTime: 1h + # - !type:JobRequirementLoadoutEffect + # requirement: + # !type:RoleTimeRequirement + # role: JobLibrarian + # time: 3600 # 1hr same as Jamjar. + # End DEN storage: back: - Cane From 921dea673bbcaaa86e2b873091bd260e6633712c Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:16:45 -0500 Subject: [PATCH 39/54] obsoleted and refactored a lot of bullshsit --- .../JobRequirementsManager.cs | 27 ++++++- .../PlayTimeTracking/JobRequirementManager.cs | 30 +++++++ .../PlayTimeTrackingSystem.cs | 72 +++++++++++------ .../PlayTimeTrackingSystem.cs | 40 ++++++++++ Content.Shared/Roles/AntagPrototype.cs | 10 +++ Content.Shared/Roles/JobPrototype.cs | 10 +++ .../Roles/JobRequirementOverridePrototype.cs | 19 +++++ Content.Shared/Roles/SharedRoleSystem.cs | 6 +- .../SharedPlayerRequirementManager.cs | 39 +++++++++ Content.Shared/_DEN/Roles/SharedRoleSystem.cs | 80 +++++++++++++++++++ 10 files changed, 308 insertions(+), 25 deletions(-) create mode 100644 Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs create mode 100644 Content.Server/_DEN/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs create mode 100644 Content.Shared/_DEN/Roles/SharedRoleSystem.cs diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index 9325507c53..94edcc3746 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Content.Shared._DEN.Requirements.Managers; using Content.Shared.CCVar; using Content.Shared.Players; using Content.Shared.Players.JobWhitelist; @@ -15,7 +16,7 @@ namespace Content.Client.Players.PlayTimeTracking; -public sealed class JobRequirementsManager : ISharedPlaytimeManager +public sealed partial class JobRequirementsManager : ISharedPlaytimeManager // DEN: Make partial { [Dependency] private readonly IBaseClient _client = default!; [Dependency] private readonly IClientNetManager _net = default!; @@ -23,6 +24,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager [Dependency] private readonly IEntityManager _entManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypes = default!; + [Dependency] private readonly IPlayerRequirementManager _requirements = default!; // DEN private readonly Dictionary _roles = new(); private readonly List> _jobBans = new(); @@ -148,10 +150,21 @@ public bool IsAllowed( return false; // Check other role requirements + // TODO DEN: This is deprecated var reqs = _entManager.System().GetRoleRequirements(job); if (!CheckRoleRequirements(reqs, profile, out reason)) return false; + // Begin DEN: Use player requirements + var roleSystem = _entManager.System(); + var requirements = roleSystem.GetRolePlayerRequirements(job); + if (requirements != null && !PassesRequirements(profile, requirements)) + { + reason = SharedPlayerRequirementManager.GetCombinedReason(requirements); + return false; + } + // End DEN + return true; } @@ -175,14 +188,26 @@ public bool IsAllowed( return false; // Check other role requirements + // TODO DEN: This is deprecated var reqs = _entManager.System().GetRoleRequirements(antag); if (!CheckRoleRequirements(reqs, profile, out reason)) return false; + // Begin DEN: Use player requirements + var roleSystem = _entManager.System(); + var requirements = roleSystem.GetRolePlayerRequirements(antag); + if (requirements != null && !PassesRequirements(profile, requirements)) + { + reason = SharedPlayerRequirementManager.GetCombinedReason(requirements); + return false; + } + // End DEN + return true; } // This must be private so code paths can't accidentally skip requirement overrides. Call this through IsAllowed() + [Obsolete("Use SharedPlayerRequirementManager.CheckRequirements() instead")] // DEN private bool CheckRoleRequirements(HashSet? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason) { reason = null; diff --git a/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs new file mode 100644 index 0000000000..3515f3211d --- /dev/null +++ b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs @@ -0,0 +1,30 @@ +using Content.Shared._DEN.Requirements.Managers; +using Content.Shared._DEN.Requirements.PlayerRequirements; +using Content.Shared.Preferences; +using Content.Shared.Roles; + +namespace Content.Client.Players.PlayTimeTracking; + +public sealed partial class JobRequirementsManager +{ + /// + /// Check if we pass a given list of requirements. + /// A profile may be optionally supplied to replace the one in the context. + /// + /// + /// + /// + private bool PassesRequirements(HumanoidCharacterProfile? profile, + List requirements) + { + var session = _playerManager.LocalSession; + var context = session != null + ? _requirements.GetPlayerContext(session) + : new(); + + if (profile != null) + context.Profile = profile; + + return SharedPlayerRequirementManager.CheckRequirements(context, requirements); + } +} diff --git a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs index 7d5f177a23..be17e9a5f8 100644 --- a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs +++ b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs @@ -7,6 +7,7 @@ using Content.Server.GameTicking.Events; using Content.Server.Preferences.Managers; using Content.Server.Station.Events; +using Content.Shared._DEN.Requirements.Managers; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Content.Shared.Mobs; @@ -27,7 +28,7 @@ namespace Content.Server.Players.PlayTimeTracking; /// /// Connects to the simulation state. Reports trackers and such. /// -public sealed class PlayTimeTrackingSystem : EntitySystem +public sealed partial class PlayTimeTrackingSystem : EntitySystem // DEN: Make partial { [Dependency] private readonly IAdminManager _adminManager = default!; [Dependency] private readonly IAfkManager _afk = default!; @@ -35,6 +36,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IServerPreferencesManager _preferencesManager = default!; [Dependency] private readonly IPrototypeManager _prototypes = default!; + [Dependency] private readonly IPlayerRequirementManager _requirements = default!; // DEN [Dependency] private readonly SharedRoleSystem _roles = default!; [Dependency] private readonly PlayTimeTrackingManager _tracking = default!; @@ -249,15 +251,25 @@ public bool IsAllowed(ICommonSession player, ProtoId job) playTimes = new Dictionary(); } + // Begin DEN: This is a guard clause now. var requirements = _roles.GetRoleRequirements(job); - return JobRequirements.TryRequirementsMet( + if (!JobRequirements.TryRequirementsMet( requirements, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) - _preferencesManager.GetPreferences(player.UserId).SelectedCharacter); + _preferencesManager.GetPreferences(player.UserId).SelectedCharacter)) + return false; + // End DEN + // Begin DEN: Use PlayerRequirements + var playerReqs = _roles.GetRolePlayerRequirements(job); + if (playerReqs != null && !PassesRequirements(player, playerReqs)) + return false; + + return true; + // End DEN } /// @@ -277,15 +289,25 @@ public bool IsAllowed(ICommonSession player, ProtoId antag) playTimes = new Dictionary(); } + // Begin DEN: This is a guard clause now. var requirements = _roles.GetRoleRequirements(antag); - return JobRequirements.TryRequirementsMet( + if (!JobRequirements.TryRequirementsMet( requirements, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) - _preferencesManager.GetPreferences(player.UserId).SelectedCharacter); + _preferencesManager.GetPreferences(player.UserId).SelectedCharacter)) + return false; + // End DEN + // Begin DEN: Use PlayerRequirements + var playerReqs = _roles.GetRolePlayerRequirements(antag); + if (playerReqs != null && !PassesRequirements(player, playerReqs)) + return false; + + return true; + // End DEN } public HashSet> GetDisallowedJobs(ICommonSession player) @@ -294,18 +316,21 @@ public HashSet> GetDisallowedJobs(ICommonSession player) if (!_cfg.GetCVar(CCVars.GameRoleTimers)) return roles; - if (!_tracking.TryGetTrackerTimes(player, out var playTimes)) - { - Log.Error($"Unable to check playtimes {Environment.StackTrace}"); - playTimes = new Dictionary(); - } + // DEN: Commented out because we're using contexts now + // if (!_tracking.TryGetTrackerTimes(player, out var playTimes)) + // { + // Log.Error($"Unable to check playtimes {Environment.StackTrace}"); + // playTimes = new Dictionary(); + // } + // Begin DEN: Use PlayerRequirements + var context = _requirements.GetPlayerContext(player); foreach (var job in _prototypes.EnumeratePrototypes()) { - if (JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter)) + if (IsJobAllowed(player, job, context)) roles.Add(job.ID); } - + // End DEN return roles; } @@ -315,20 +340,21 @@ public void RemoveDisallowedJobs(NetUserId userId, List> j return; var player = _playerManager.GetSessionById(userId); - if (!_tracking.TryGetTrackerTimes(player, out var playTimes)) - { - // Sorry mate but your playtimes haven't loaded. - Log.Error($"Playtimes weren't ready yet for {player} on roundstart!"); - playTimes ??= new Dictionary(); - } - + // DEN: Commented out because we're using contexts now + // if (!_tracking.TryGetTrackerTimes(player, out var playTimes)) + // { + // // Sorry mate but your playtimes haven't loaded. + // Log.Error($"Playtimes weren't ready yet for {player} on roundstart!"); + // playTimes ??= new Dictionary(); + // } + + // Begin DEN: Use PlayerRequirements + var context = _requirements.GetPlayerContext(player); for (var i = 0; i < jobs.Count; i++) { - if (_prototypes.Resolve(jobs[i], out var job) - && JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(userId).SelectedCharacter)) - { + if (_prototypes.Resolve(jobs[i], out var job) && IsJobAllowed(player, job, context)) continue; - } + // End DEN jobs.RemoveSwap(i); i--; diff --git a/Content.Server/_DEN/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs b/Content.Server/_DEN/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs new file mode 100644 index 0000000000..8cdc7c57e4 --- /dev/null +++ b/Content.Server/_DEN/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs @@ -0,0 +1,40 @@ +using Content.Shared._DEN.Requirements.Managers; +using Content.Shared._DEN.Requirements.PlayerRequirements; +using Content.Shared.Preferences; +using Content.Shared.Roles; +using Robust.Shared.Player; + +namespace Content.Server.Players.PlayTimeTracking; + +public sealed partial class PlayTimeTrackingSystem +{ + private bool PassesRequirements(ICommonSession player, + List requirements, + PlayerRequirementContext? context = null) + { + context ??= _requirements.GetPlayerContext(player); + return SharedPlayerRequirementManager.CheckRequirements(context, requirements); + } + + private bool IsJobAllowed(ICommonSession player, + JobPrototype job, + PlayerRequirementContext? context = null) + { + context ??= _requirements.GetPlayerContext(player); + var playTimes = context.Playtimes ?? new Dictionary(); + var profile = context.Profile + ?? (HumanoidCharacterProfile?)_preferencesManager.GetPreferences(player.UserId).SelectedCharacter; + + // TODO DEN: This is deprecated. + var oldRequirementsMet = JobRequirements.TryRequirementsMet(job, + playTimes, + out _, + EntityManager, + _prototypes, + profile); + + var playerReqs = _roles.GetRolePlayerRequirements(job); + var requirementsMet = playerReqs == null || PassesRequirements(player, playerReqs, context); + return oldRequirementsMet && requirementsMet; + } +} diff --git a/Content.Shared/Roles/AntagPrototype.cs b/Content.Shared/Roles/AntagPrototype.cs index 367b05c3dd..3d12856e8a 100644 --- a/Content.Shared/Roles/AntagPrototype.cs +++ b/Content.Shared/Roles/AntagPrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared._DEN.Requirements.PlayerRequirements; using Content.Shared.Guidebook; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -48,8 +49,17 @@ public sealed partial class AntagPrototype : IPrototype /// Requirements that must be met to opt in to this antag role. /// [DataField, Access(typeof(SharedRoleSystem), Other = AccessPermissions.None)] + [Obsolete("Use PlayerRequirements instead")] // DEN public HashSet? Requirements; + // Begin DEN: Use PlayerRequirements + /// + /// A list of PlayerRequirements for this role. + /// + [DataField("playerRequirements")] + public List? PlayerRequirements; + // End DEN + /// /// Optional list of guides associated with this antag. If the guides are opened, the first entry in this list /// will be used to select the currently selected guidebook. diff --git a/Content.Shared/Roles/JobPrototype.cs b/Content.Shared/Roles/JobPrototype.cs index 6b58c42428..2883b9b395 100644 --- a/Content.Shared/Roles/JobPrototype.cs +++ b/Content.Shared/Roles/JobPrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared._DEN.Requirements.PlayerRequirements; using Content.Shared.Access; using Content.Shared.Guidebook; using Content.Shared.Players.PlayTimeTracking; @@ -48,8 +49,17 @@ public sealed partial class JobPrototype : IPrototype /// Requirements for the job. /// [DataField, Access(typeof(SharedRoleSystem), Other = AccessPermissions.None)] + [Obsolete("Use PlayerRequirements instead")] // DEN public HashSet? Requirements; + // Begin DEN: Use PlayerRequirements + /// + /// A list of PlayerRequirements for this job. + /// + [DataField("playerRequirements")] + public List? PlayerRequirements; + // End DEN + /// /// When true - the station will have anouncement about arrival of this player. /// diff --git a/Content.Shared/Roles/JobRequirementOverridePrototype.cs b/Content.Shared/Roles/JobRequirementOverridePrototype.cs index d0ce649f36..9c43a63d64 100644 --- a/Content.Shared/Roles/JobRequirementOverridePrototype.cs +++ b/Content.Shared/Roles/JobRequirementOverridePrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared._DEN.Requirements.PlayerRequirements; using Robust.Shared.Prototypes; namespace Content.Shared.Roles; @@ -13,8 +14,26 @@ public sealed partial class JobRequirementOverridePrototype : IPrototype public string ID { get; private set; } = default!; [DataField] + [Obsolete("Use JobRequirements instead")] // DEN public Dictionary, HashSet> Jobs = new (); [DataField] + [Obsolete("Use AntagRequirements instead")] // DEN public Dictionary, HashSet> Antags = new (); + + // Begin DEN: Use PlayerRequirements + + /// + /// A dictionary of job roles mapped to a list of overriding requirements. + /// + [DataField] + public Dictionary, List> JobRequirements = new(); + + /// + /// A dictionary of antagonist roles mapped to a list of overriding requirements. + /// + [DataField] + public Dictionary, List> AntagRequirements = new(); + + // End DEN } diff --git a/Content.Shared/Roles/SharedRoleSystem.cs b/Content.Shared/Roles/SharedRoleSystem.cs index 47db09ebdb..29046f1e42 100644 --- a/Content.Shared/Roles/SharedRoleSystem.cs +++ b/Content.Shared/Roles/SharedRoleSystem.cs @@ -17,7 +17,7 @@ namespace Content.Shared.Roles; -public abstract class SharedRoleSystem : EntitySystem +public abstract partial class SharedRoleSystem : EntitySystem // DEN: Make partial { [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; @@ -672,6 +672,7 @@ public void MindPlaySound(EntityUid mindId, SoundSpecifier? sound, MindComponent /// /// Returns the list of requirements for a role, or null. May be altered by requirement overrides. /// + [Obsolete("Use GetRolePlayerRequirements instead")] // DEN public HashSet? GetRoleRequirements(JobPrototype job) { if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job.ID, out var req)) @@ -682,6 +683,7 @@ public void MindPlaySound(EntityUid mindId, SoundSpecifier? sound, MindComponent // TODO ROLES Change to readonly? /// + [Obsolete("Use GetRolePlayerRequirements instead")] // DEN public HashSet? GetRoleRequirements(AntagPrototype antag) { if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag.ID, out var req)) @@ -692,6 +694,7 @@ public void MindPlaySound(EntityUid mindId, SoundSpecifier? sound, MindComponent // TODO ROLES Change to readonly? /// + [Obsolete("Use GetRolePlayerRequirements instead")] // DEN public HashSet? GetRoleRequirements(ProtoId jobId) { return _prototypes.TryIndex(jobId, out var job) ? GetRoleRequirements(job) : null; @@ -699,6 +702,7 @@ public void MindPlaySound(EntityUid mindId, SoundSpecifier? sound, MindComponent // TODO ROLES Change to readonly? /// + [Obsolete("Use GetRolePlayerRequirements instead")] // DEN public HashSet? GetRoleRequirements(ProtoId antagId) { return _prototypes.TryIndex(antagId, out var antag) ? GetRoleRequirements(antag) : null; diff --git a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs index adbfef012f..6fda943220 100644 --- a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs +++ b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs @@ -1,6 +1,8 @@ +using System.Text; using Content.Shared._DEN.Requirements.PlayerRequirements; using JetBrains.Annotations; using Robust.Shared.Player; +using Robust.Shared.Utility; namespace Content.Shared._DEN.Requirements.Managers; @@ -66,6 +68,43 @@ public static bool CheckRequirement(PlayerRequirementContext context, IPlayerReq return true; } + /// + /// Get a combined reason message for an enumerable collection of requirements. + /// + /// A collection of requirements. + /// A formatted message containing all the requirement reasons. + [PublicAPI] + public static FormattedMessage GetCombinedReason(IEnumerable requirements) + { + var messageBuilder = new StringBuilder(); + foreach (var req in requirements) + messageBuilder.AppendLine(req.GetReason()); + + var messageString = messageBuilder.ToString().Trim(); + return FormattedMessage.FromMarkupPermissive(messageString); + } + + /// + /// Get a combined reason message for an enumerable collection of requirements. + /// This override will only include requirements that fail, given a context. + /// + /// A context to check against the requirements. + /// A collection of requirements. + /// A formatted message containing all the requirement reasons. + [PublicAPI] + public static FormattedMessage GetCombinedReason(PlayerRequirementContext context, IEnumerable requirements) + { + var messageBuilder = new StringBuilder(); + foreach (var req in requirements) + { + if (!CheckRequirement(context, req)) + messageBuilder.AppendLine(req.GetReason()); + } + + var messageString = messageBuilder.ToString().Trim(); + return FormattedMessage.FromMarkupPermissive(messageString); + } + /// public abstract PlayerRequirementContext GetPlayerContext(ICommonSession session); } diff --git a/Content.Shared/_DEN/Roles/SharedRoleSystem.cs b/Content.Shared/_DEN/Roles/SharedRoleSystem.cs new file mode 100644 index 0000000000..2f1cee4f74 --- /dev/null +++ b/Content.Shared/_DEN/Roles/SharedRoleSystem.cs @@ -0,0 +1,80 @@ +using Content.Shared._DEN.Requirements.PlayerRequirements; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Roles; + +public abstract partial class SharedRoleSystem +{ + /// + /// Get a list of requirements associated with a given job. + /// + /// + /// If a role requirement override is set for this job, the requirements associated + /// with the job in the override will be provided instead. + /// + /// The job to get player requirements for. + /// The job's actual requirements, accounting for the requirement CVar. + [PublicAPI] + public List? GetRolePlayerRequirements(JobPrototype job) + { + if (_requirementOverride != null + && _requirementOverride.JobRequirements.TryGetValue(job.ID, out var overrides)) + return overrides; + + return job.PlayerRequirements; + } + + /// + /// Get a list of requirements associated with a given antagonist role. + /// + /// + /// If a role requirement override is set for this role, the requirements associated + /// with the role in the override will be provided instead. + /// + /// The antagonist role to get player requirements for. + /// The role's actual requirements, accounting for the requirement CVar. + [PublicAPI] + public List? GetRolePlayerRequirements(AntagPrototype antag) + { + if (_requirementOverride != null + && _requirementOverride.AntagRequirements.TryGetValue(antag.ID, out var overrides)) + return overrides; + + return antag.PlayerRequirements; + } + + /// + /// Get a list of requirements associated with a given job. + /// + /// + /// If a role requirement override is set for this job, the requirements associated + /// with the job in the override will be provided instead. + /// + /// The ID of a job to get player requirements for. + /// The job's actual requirements, accounting for the requirement CVar. + [PublicAPI] + public List? GetRolePlayerRequirements(ProtoId jobId) + { + return _prototypes.TryIndex(jobId, out var job) + ? GetRolePlayerRequirements(job) + : null; + } + + /// + /// Get a list of requirements associated with a given antagonist role. + /// + /// + /// If a role requirement override is set for this role, the requirements associated + /// with the role in the override will be provided instead. + /// + /// The Id of an antagonist role to get player requirements for. + /// The role's actual requirements, accounting for the requirement CVar. + [PublicAPI] + public List? GetRolePlayerRequirements(ProtoId antagId) + { + return _prototypes.TryIndex(antagId, out var antag) + ? GetRolePlayerRequirements(antag) + : null; + } +} From b0a3c6ee045a2965e3b8a24a09d8776e386f262d Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:38:01 -0500 Subject: [PATCH 40/54] add debug traits for requirements --- Resources/Locale/en-US/_DEN/traits/debug.ftl | 2 + .../Prototypes/_DEN/EntityTraits/debug.yml | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/Resources/Locale/en-US/_DEN/traits/debug.ftl b/Resources/Locale/en-US/_DEN/traits/debug.ftl index 00267de3ee..b9cb00f369 100644 --- a/Resources/Locale/en-US/_DEN/traits/debug.ftl +++ b/Resources/Locale/en-US/_DEN/traits/debug.ftl @@ -1,3 +1,5 @@ trait-debug-jittery-name = Jittery trait-debug-stunned-name = Stunned trait-debug-spawn-crowbars-name = Spawn Crowbars +trait-debug-requirements-playtime-name = Playtime Requirements +trait-debug-requirements-profile-name = Profile Requirements diff --git a/Resources/Prototypes/_DEN/EntityTraits/debug.yml b/Resources/Prototypes/_DEN/EntityTraits/debug.yml index b2435e9035..1cb2697f61 100644 --- a/Resources/Prototypes/_DEN/EntityTraits/debug.yml +++ b/Resources/Prototypes/_DEN/EntityTraits/debug.yml @@ -32,3 +32,41 @@ - Crowbar - Crowbar - Crowbar + +- type: entityTrait + id: DebugRequirementsPlaytime + name: trait-debug-requirements-playtime-name + characterEditorSelectable: false + functions: [] + requirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Command + minTime: 86400 + - !type:PlayerJobPlaytimeRequirement + job: Clown + maxTime: 8340 + - !type:PlayerOverallPlaytimeRequirement + minTime: 3600 + maxTime: 14400 + +- type: entityTrait + id: DebugRequirementsProfile + name: trait-debug-requirements-profile-name + characterEditorSelectable: false + functions: [] + requirements: + - !type:PlayerAgeRequirement + max: 60 + - !type:PlayerSpeciesRequirement + species: + - Human + - Vulpkanin + - SlimePerson + - !type:PlayerTraitRequirement + inverted: true + traits: + - PoorVision + - Blindness + count: !type:RangeCountRequirement + min: 2 + max: 8 From dc7b9299190549bb61333cb2d5b9af2b659639e7 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:12:13 -0500 Subject: [PATCH 41/54] add comments to these fields --- .../_DEN/Players/PlayTimeTracking/JobRequirementManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs index 3515f3211d..9de5ae08af 100644 --- a/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs +++ b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs @@ -11,9 +11,9 @@ public sealed partial class JobRequirementsManager /// Check if we pass a given list of requirements. /// A profile may be optionally supplied to replace the one in the context. /// - /// - /// - /// + /// A profile associated with the character we're loading in. + /// The requirements to check. + /// Whether we pass the requirements given. private bool PassesRequirements(HumanoidCharacterProfile? profile, List requirements) { From 0fcd30e26c5a9af2ab943c19870adc383ed5d263 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:18:08 -0500 Subject: [PATCH 42/54] remove "shouldHide" traits from characters --- Content.Shared/Preferences/HumanoidCharacterProfile.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index 86252c1a53..6017de228d 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -616,7 +616,9 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection context.Profile = this; var traits = EntityTraitPreferences - .Where(t => prototypeManager.TryIndex(t, out var trait) && trait.Selectable) + .Where(t => prototypeManager.TryIndex(t, out var trait) + && trait.Selectable + && !SharedPlayerRequirementManager.ShouldHide(context, trait.Requirements)) .ToList(); // End DEN From de4b86e97ed26c481f0930c34c836a0a4f1abcd9 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:35:18 -0500 Subject: [PATCH 43/54] include context in player requirement reasons --- .../UI/Traits/EntityTraitSelector.xaml.cs | 29 ++++++++++++++----- .../Effects/PlayerRequirementLoadoutEffect.cs | 2 +- .../SharedPlayerRequirementManager.cs | 11 +++---- .../PlayerRequirements/CountRequirement.cs | 12 ++++---- .../PlayerRequirements/IPlayerRequirement.cs | 5 ++-- .../PlayerRequirements.Playtime.cs | 6 ++-- .../PlayerRequirements.Profile.cs | 8 ++--- 7 files changed, 46 insertions(+), 27 deletions(-) diff --git a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs index ba49b0bb79..ea6e832c62 100644 --- a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs +++ b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs @@ -57,16 +57,26 @@ private void SetTrait(EntityTraitPrototype trait) } /// - /// Toggles the visibility of this selector, depending on the requirements of a given trait. + /// Retrieve this session's context. /// - /// The trait to check the requirements of. - private void UpdateVisibility(EntityTraitPrototype trait) + /// The local player's session context. + private PlayerRequirementContext GetContext() { var session = _player.LocalSession; if (session == null) - return; + return new(); var context = _requirements.GetPlayerContext(session); + return context; + } + + /// + /// Toggles the visibility of this selector, depending on the requirements of a given trait. + /// + /// The trait to check the requirements of. + private void UpdateVisibility(EntityTraitPrototype trait) + { + var context = GetContext(); Visible = !SharedPlayerRequirementManager.ShouldHide(context, trait.Requirements); } @@ -80,7 +90,11 @@ private void UpdateVisibility(EntityTraitPrototype trait) if (_trait is null) return null; - var tooltipString = ConstructTooltipDescription(_trait); + var context = _trait.Requirements.Count > 0 + ? GetContext() + : null; + + var tooltipString = ConstructTooltipDescription(_trait, context); if (tooltipString.Length == 0) return null; @@ -96,7 +110,8 @@ private void UpdateVisibility(EntityTraitPrototype trait) /// /// The trait to construct a description out of. /// The tooltip text associated with this trait. - private static string ConstructTooltipDescription(EntityTraitPrototype trait) + private static string ConstructTooltipDescription(EntityTraitPrototype trait, + PlayerRequirementContext? context = null) { var tooltipBuilder = new StringBuilder(); @@ -110,7 +125,7 @@ private static string ConstructTooltipDescription(EntityTraitPrototype trait) tooltipBuilder.AppendLine(); // Empty line foreach (var requirement in trait.Requirements) { - var reason = requirement.GetReason(); + var reason = requirement.GetReason(context); if (reason is null) continue; diff --git a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs index 269e323c67..e5d4e7b159 100644 --- a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs +++ b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs @@ -38,7 +38,7 @@ public override bool Validate(HumanoidCharacterProfile profile, if (!success) { - var reasonText = Requirement.GetReason() ?? string.Empty; + var reasonText = Requirement.GetReason(context) ?? string.Empty; reason = FormattedMessage.FromMarkupPermissive(reasonText); return false; } diff --git a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs index 6fda943220..26175b955d 100644 --- a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs +++ b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs @@ -72,13 +72,14 @@ public static bool CheckRequirement(PlayerRequirementContext context, IPlayerReq /// Get a combined reason message for an enumerable collection of requirements. /// /// A collection of requirements. + /// A context to check against the requirements. /// A formatted message containing all the requirement reasons. [PublicAPI] - public static FormattedMessage GetCombinedReason(IEnumerable requirements) + public static FormattedMessage GetCombinedReason(IEnumerable requirements, PlayerRequirementContext? context = null) { var messageBuilder = new StringBuilder(); foreach (var req in requirements) - messageBuilder.AppendLine(req.GetReason()); + messageBuilder.AppendLine(req.GetReason(context)); var messageString = messageBuilder.ToString().Trim(); return FormattedMessage.FromMarkupPermissive(messageString); @@ -86,19 +87,19 @@ public static FormattedMessage GetCombinedReason(IEnumerable /// /// Get a combined reason message for an enumerable collection of requirements. - /// This override will only include requirements that fail, given a context. + /// This function will only include requirements that fail, given a context. /// /// A context to check against the requirements. /// A collection of requirements. /// A formatted message containing all the requirement reasons. [PublicAPI] - public static FormattedMessage GetCombinedReason(PlayerRequirementContext context, IEnumerable requirements) + public static FormattedMessage GetFailedCombinedReason(PlayerRequirementContext context, IEnumerable requirements) { var messageBuilder = new StringBuilder(); foreach (var req in requirements) { if (!CheckRequirement(context, req)) - messageBuilder.AppendLine(req.GetReason()); + messageBuilder.AppendLine(req.GetReason(context)); } var messageString = messageBuilder.ToString().Trim(); diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs index 0f0a105f17..5021c52255 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Shared._DEN.Requirements.Managers; using JetBrains.Annotations; namespace Content.Shared._DEN.Requirements.PlayerRequirements; @@ -20,8 +21,9 @@ public abstract partial class CountRequirement /// This would slot into the sentence "Must have [reason] of the following items: [items]". /// /// "At least 1", "between 2 and 5", "all" + /// An optional context used to add additional information to the reason. /// A string representation of this range's requirement bounds. - public abstract string GetReason(); + public abstract string GetReason(PlayerRequirementContext? context = null); /// /// Check if our currently-selected items in a collection meets this @@ -58,7 +60,7 @@ public sealed partial class ConstantCountRequirement : CountRequirement public int Count; /// - public override string GetReason() + public override string GetReason(PlayerRequirementContext? context = null) { // "Must have exactly 1 of the following items." return Loc.GetString("count-requirement-constant-reason", @@ -91,7 +93,7 @@ public sealed partial class RangeCountRequirement : CountRequirement, IPlayerRan public int? Max { get; set; } = null; /// - public override string GetReason() + public override string GetReason(PlayerRequirementContext? context = null) { if (this is IPlayerRangeRequirement range) return range.GetRangeConstraintReason(); @@ -133,7 +135,7 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req public sealed partial class AnyCountRequirement : CountRequirement { /// - public override string GetReason() + public override string GetReason(PlayerRequirementContext? context = null) { // "Must have any of the following items." return Loc.GetString("count-requirement-any-reason"); @@ -153,7 +155,7 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req public sealed partial class AllCountRequirement : CountRequirement { /// - public override string GetReason() + public override string GetReason(PlayerRequirementContext? context = null) { // "Must have all of the following items." return Loc.GetString("count-requirement-all-reason"); diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs index a1cc4bb623..693cfa68a9 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs @@ -66,8 +66,9 @@ public partial interface IPlayerRequirement /// /// This should display differently depending on the value of . /// + /// An optional context used to add additional information to the reason. /// An optional string representing the requirement text to display to a player. - string? GetReason(); + string? GetReason(PlayerRequirementContext? context = null); } /// @@ -88,7 +89,7 @@ public abstract partial class PlayerRequirement : IPlayerRequirement public abstract bool CheckRequirement(PlayerRequirementContext context); /// - public abstract string? GetReason(); + public abstract string? GetReason(PlayerRequirementContext? context = null); /// public abstract bool PreCheck(PlayerRequirementContext context); diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs index 23baf5403a..2615819143 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -154,7 +154,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) } /// - public override string? GetReason() + public override string? GetReason(PlayerRequirementContext? context = null) { // Do not give a reason if role timers are disabled. if (ShouldAutoPass()) @@ -253,7 +253,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) } /// - public override string? GetReason() + public override string? GetReason(PlayerRequirementContext? context = null) { // Do not give a reason if role timers are disabled. if (ShouldAutoPass()) @@ -362,7 +362,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) } /// - public override string? GetReason() + public override string? GetReason(PlayerRequirementContext? context = null) { // Do not give a reason if role timers are disabled. if (ShouldAutoPass()) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs index f0aad45a13..09df8b3e39 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs @@ -43,7 +43,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) } /// - public override string? GetReason() + public override string? GetReason(PlayerRequirementContext? context = null) { if (!TryGetRangeConstraintReason(out var constraintReason)) return null; @@ -109,7 +109,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) } /// - public override string? GetReason() + public override string? GetReason(PlayerRequirementContext? context = null) { var protoMan = IoCManager.Resolve(); @@ -174,12 +174,12 @@ public override bool CheckRequirement(PlayerRequirementContext context) } /// - public override string? GetReason() + public override string? GetReason(PlayerRequirementContext? context = null) { var protoMan = IoCManager.Resolve(); var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan)); var traitList = string.Join(", ", traitNames); - var constraintReason = Count.GetReason(); + var constraintReason = Count.GetReason(context); return Loc.GetString("player-requirement-trait-reason", ("inverted", Inverted), From e2775dad519680b7e1c5aae999f41438f88b6a36 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:44:04 -0500 Subject: [PATCH 44/54] add difference ranges to player range requirements --- .../PlayerRequirements/CountRequirement.cs | 33 ++- .../PlayerRequirements/IPlayerRequirement.cs | 88 ++++++- .../PlayerRequirements.Playtime.cs | 249 +++++++++++------- .../PlayerRequirements.Profile.cs | 69 ++++- .../requirements/playtime-requirements.ftl | 2 +- .../_DEN/requirements/requirement-range.ftl | 5 + 6 files changed, 324 insertions(+), 122 deletions(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs index 5021c52255..ff3c2b8862 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs @@ -23,7 +23,7 @@ public abstract partial class CountRequirement /// "At least 1", "between 2 and 5", "all" /// An optional context used to add additional information to the reason. /// A string representation of this range's requirement bounds. - public abstract string GetReason(PlayerRequirementContext? context = null); + public abstract string GetReason(); /// /// Check if our currently-selected items in a collection meets this @@ -60,7 +60,7 @@ public sealed partial class ConstantCountRequirement : CountRequirement public int Count; /// - public override string GetReason(PlayerRequirementContext? context = null) + public override string GetReason() { // "Must have exactly 1 of the following items." return Loc.GetString("count-requirement-constant-reason", @@ -93,7 +93,7 @@ public sealed partial class RangeCountRequirement : CountRequirement, IPlayerRan public int? Max { get; set; } = null; /// - public override string GetReason(PlayerRequirementContext? context = null) + public override string GetReason() { if (this is IPlayerRangeRequirement range) return range.GetRangeConstraintReason(); @@ -113,15 +113,30 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req } /// - public string? GetMinText() + public string FormatValue(int value) { - return Min?.ToString(); + return value.ToString(); } /// - public string? GetMaxText() + public int? GetDifference(int value) { - return Max?.ToString(); + // Unimplemented - unused + throw new NotImplementedException(); + } + + /// + public int Sign(int difference) + { + // Unimplemented - unused + throw new NotImplementedException(); + } + + /// + public string FormatDifferenceText(int difference) + { + // Unimplemented - unused + throw new NotImplementedException(); } } @@ -135,7 +150,7 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req public sealed partial class AnyCountRequirement : CountRequirement { /// - public override string GetReason(PlayerRequirementContext? context = null) + public override string GetReason() { // "Must have any of the following items." return Loc.GetString("count-requirement-any-reason"); @@ -155,7 +170,7 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req public sealed partial class AllCountRequirement : CountRequirement { /// - public override string GetReason(PlayerRequirementContext? context = null) + public override string GetReason() { // "Must have all of the following items." return Loc.GetString("count-requirement-all-reason"); diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs index 693cfa68a9..01b83c44c2 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs @@ -133,7 +133,7 @@ bool IsInRange(T value) /// /// Get a reason message to display to the player for this requirement's allowed value range. /// - string GetRangeConstraintReason() + string GetRangeConstraintReason(PlayerRequirementContext? context = null) { var minText = GetMinText(); var maxText = GetMaxText(); @@ -157,13 +157,95 @@ string GetRangeConstraintReason() }; } + /// + /// Get the difference between this value and the maximum or minimum value of this range. + /// Should return null if the value is in range. + /// + /// The value to get a difference of. + /// The difference between the value and the nearest bound of this range. + T? GetDifference(T value); + + /// + /// Get the "sign" associated with this difference. + /// + /// + /// "1" means the difference is lower than the minimum. + /// "-1" means the difference is higher than the maximum. + /// "0" means the difference is within range. + /// + /// The difference value. + /// The sign of this difference. + int Sign(T difference); + + /// + /// Format this difference value as a string to display to the player. + /// + /// The difference value. + /// A string representation of this difference. + string FormatDifferenceText(T difference); + + /// + /// Format a "difference" value depending on if it is higher than the maximum or lower than the minimum. + /// + /// The difference value to format. + /// A string representation of this difference. + string FormatDifference(T difference) + { + var diffText = FormatDifferenceText(difference); + return Sign(difference) switch + { + 1 => Loc.GetString("player-requirement-range-difference-lower", ("count", diffText)), + -1 => Loc.GetString("player-requirement-range-difference-higher", ("count", diffText)), + _ => "0", + }; + } + + /// + /// Get a string representation between this value and its distance from the nearest bound. + /// Returns null if the value is in range. + /// + /// A value being tested against the requirement. + /// A string representation of the difference between the value and the requirement's bounds. + string? GetDifferenceReason(T value) + { + var difference = GetDifference(value); + if (difference == null) + return null; + + var diffValue = FormatDifference(difference.Value) ?? difference.Value.ToString(); + if (diffValue == null) + return null; + + return Loc.GetString("player-requirement-range-difference", + ("difference", diffValue)); + } + + /// + /// Format a value of the range type into a string. + /// + /// The value to format. + /// A string representation of this value. + string FormatValue(T value); + /// /// Get a string representation of this range's minimum value. /// - string? GetMinText(); + string? GetMinText() + { + if (Min == null) + return null; + + return FormatValue(Min.Value); + } /// /// Get a string representation of this range's maximum value. /// - string? GetMaxText(); + string? GetMaxText() + { + if (Max == null) + return null; + + return FormatValue(Max.Value); + } } diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs index 2615819143..1e4fa1c07e 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -44,6 +44,42 @@ public override bool PreCheck(PlayerRequirementContext context) return ShouldAutoPass() || context.Playtimes != null; } + /// + public override bool CheckRequirement(PlayerRequirementContext context) + { + // Auto-pass if role timers are disabled. + if (ShouldAutoPass()) + return true; + + var playtime = GetPlaytime(context); + if (playtime is null) + return false; + + return IsInRange(playtime.Value); + } + + /// + public override string? GetReason(PlayerRequirementContext? context = null) + { + // Do not give a reason if role timers are disabled. + if (ShouldAutoPass()) + return null; + + // Get the playtime constraint string. + if (!TryGetRangeConstraintReason(out var constraintReason)) + return null; + + // If there's no "difference reason", then just the constraint is fine. + // "Must have 2h of playtime overall." + if (!TryGetRangeDifferenceReason(out var differenceReason, context)) + return constraintReason; + + // "Must have 2h of playtime overall. (Need 1h more)" + return Loc.GetString("player-requirement-range-with-difference", + ("range", constraintReason), + ("difference", differenceReason)); + } + /// /// Whether or not this requirement should auto-pass. This applies if role timers /// are disabled, because playtimes shouldn't matter anyway in this case - we shouldn't @@ -70,28 +106,13 @@ public bool ShouldAutoPass() /// /// The playtime to format. /// The formatted playtime, if playtime is not null. - private static string? FormatPlaytime(TimeSpan? playtime) + private static string FormatPlaytime(TimeSpan playtime) { - if (playtime is null) - return null; - - var playtimeString = ContentLocalizationManager.FormatPlaytime(playtime.Value); + var playtimeString = ContentLocalizationManager.FormatPlaytime(playtime); return Loc.GetString("player-requirement-format-time", ("playtime", playtimeString)); } - /// - public string? GetMinText() - { - return FormatPlaytime(Min); - } - - /// - public string? GetMaxText() - { - return FormatPlaytime(Max); - } - /// /// Check if the given playtime is in range. /// @@ -105,20 +126,93 @@ protected bool IsInRange(TimeSpan playtime) return false; } + /// + public string FormatValue(TimeSpan value) + { + return FormatPlaytime(value); + } + + /// + public TimeSpan? GetDifference(TimeSpan value) + { + if (this is IPlayerRangeRequirement range && range.IsInRange(value)) + return null; + + // Negative value = greater than maximum + if (Max != null && value > Max) + return Max - value; + + // Positive value = less than minimum + if (Min != null && value < Min) + return Min - value; + + return null; + } + + /// + public int Sign(TimeSpan difference) + { + return Math.Sign(difference.TotalSeconds); + } + + /// + public string FormatDifferenceText(TimeSpan difference) + { + var diffValue = difference.Duration(); + return FormatValue(diffValue); + } + /// /// Get the text to display to the player that represents the range of valid playtimes. /// /// The playtime range description. /// Whether or not this operation was successful. - protected bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString) + protected virtual bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString) { playtimeString = null; if (this is IPlayerRangeRequirement range) - playtimeString = range.GetRangeConstraintReason(); + { + var constraintReason = range.GetRangeConstraintReason(); + playtimeString = Loc.GetString("player-requirement-playtime-constraint-reason", + ("inverted", Inverted), + ("constraint", constraintReason)); + } return playtimeString != null; } + + /// + /// Get the text to display to the player that represents the difference between their + /// playtime and the upper/lower bound of acceptable playtimes. + /// + /// A string representing the player's playtime in relation to the requirement's bounds. + /// A context that may contain this character's playtimes. + /// Whether or not this operation was successful. + protected virtual bool TryGetRangeDifferenceReason([NotNullWhen(true)] out string? reason, + PlayerRequirementContext? context = null) + { + reason = null; + + if (context?.Profile == null) + return false; + + if (this is IPlayerRangeRequirement range) + { + var playtime = GetPlaytime(context); + if (playtime != null) + reason = range.GetDifferenceReason(playtime.Value); + } + + return reason != null; + } + + /// + /// Get the playtime associated with this requirement. + /// + /// The context that may or may not contain playtimes. + /// The playtime associated with this requirement. + protected abstract TimeSpan? GetPlaytime(PlayerRequirementContext context); } [Serializable] @@ -139,43 +233,27 @@ public sealed partial class PlayerDepartmentPlaytimeRequirement : PlayerPlaytime [DataField(required: true)] public ProtoId Department = default!; - /// - public override bool CheckRequirement(PlayerRequirementContext context) + /// + protected override TimeSpan? GetPlaytime(PlayerRequirementContext context) { - // Auto-pass if role timers are disabled. - if (ShouldAutoPass()) - return true; - - var playtime = GetDepartmentPlaytime(context); - if (playtime is null) - return false; - - return IsInRange(playtime.Value); + return GetDepartmentPlaytime(context); } - /// - public override string? GetReason(PlayerRequirementContext? context = null) + /// + protected override bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString) { - // Do not give a reason if role timers are disabled. - if (ShouldAutoPass()) - return null; + if (!base.TryGetRangeConstraintReason(out playtimeString)) + return false; - // Get a formatted department name. var protoMan = IoCManager.Resolve(); var deptName = FormatDepartment(protoMan); - // Get the playtime constraint string. - if (!TryGetRangeConstraintReason(out var playtimeString)) - return null; - - var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason", - ("inverted", Inverted), - ("timeConstraint", playtimeString)); - // E.g. "You must have 2h30m of playtime in the Science department." - return Loc.GetString("player-requirement-department-playtime-reason", - ("constraint", constraintReason), + playtimeString = Loc.GetString("player-requirement-department-playtime-reason", + ("constraint", playtimeString), ("department", deptName)); + + return true; } /// @@ -238,43 +316,27 @@ public sealed partial class PlayerJobPlaytimeRequirement : PlayerPlaytimeRequire [DataField(required: true)] public ProtoId Job = default!; - /// - public override bool CheckRequirement(PlayerRequirementContext context) + /// + protected override TimeSpan? GetPlaytime(PlayerRequirementContext context) { - // Auto-pass if role timers are disabled. - if (ShouldAutoPass()) - return true; - - var playtime = GetJobPlaytime(context); - if (playtime is null) - return false; - - return IsInRange(playtime.Value); + return GetJobPlaytime(context); } - /// - public override string? GetReason(PlayerRequirementContext? context = null) + /// + protected override bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString) { - // Do not give a reason if role timers are disabled. - if (ShouldAutoPass()) - return null; + if (!base.TryGetRangeConstraintReason(out playtimeString)) + return false; - // Get the job name and format it with a color. var protoMan = IoCManager.Resolve(); var jobName = FormatJob(protoMan); - // Get the playtime constraint string. - if (!TryGetRangeConstraintReason(out var playtimeString)) - return null; - - var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason", - ("inverted", Inverted), - ("timeConstraint", playtimeString)); - // E.g. "You must have 2h30m of playtime as a Mime." - return Loc.GetString("player-requirement-job-playtime-reason", - ("constraint", constraintReason), + playtimeString = Loc.GetString("player-requirement-job-playtime-reason", + ("constraint", playtimeString), ("job", jobName)); + + return true; } /// @@ -328,18 +390,23 @@ private string FormatJob(IPrototypeManager protoMan) /// public sealed partial class PlayerOverallPlaytimeRequirement : PlayerPlaytimeRequirement { - /// - public override bool CheckRequirement(PlayerRequirementContext context) + /// + protected override TimeSpan? GetPlaytime(PlayerRequirementContext context) { - // Auto-pass if role timers are disabled. - if (ShouldAutoPass()) - return true; + return GetOverallPlaytime(context); + } - var playtime = GetOverallPlaytime(context); - if (playtime is null) + /// + protected override bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString) + { + if (!base.TryGetRangeConstraintReason(out playtimeString)) return false; - return IsInRange(playtime.Value); + // E.g. "You must have 300h of playtime overall." + playtimeString = Loc.GetString("player-requirement-overall-playtime-reason", + ("constraint", playtimeString)); + + return true; } /// @@ -360,24 +427,4 @@ public override bool CheckRequirement(PlayerRequirementContext context) return playtime; } - - /// - public override string? GetReason(PlayerRequirementContext? context = null) - { - // Do not give a reason if role timers are disabled. - if (ShouldAutoPass()) - return null; - - // Get the playtime constraint string. - if (!TryGetRangeConstraintReason(out var playtimeString)) - return null; - - var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason", - ("inverted", Inverted), - ("timeConstraint", playtimeString)); - - // E.g. "You must have 300h of playtime overall." - return Loc.GetString("player-requirement-overall-playtime-reason", - ("constraint", constraintReason)); - } } diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs index 09df8b3e39..568c6ad790 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs @@ -49,15 +49,22 @@ public override bool CheckRequirement(PlayerRequirementContext context) return null; // "Must be between 20 and 40 years old." - return Loc.GetString("player-requirement-age-reason", + var rangeReason = Loc.GetString("player-requirement-age-reason", ("inverted", Inverted), ("constraint", constraintReason)); + + if (!TryGetRangeDifferenceReason(out var differenceReason, context)) + return rangeReason; + + return Loc.GetString("player-requirement-range-with-difference", + ("range", rangeReason), + ("difference", differenceReason)); } /// /// Get the text to display to the player that represents the range of valid playtimes. /// - /// The playtime range description. + /// The age range description. /// Whether or not this operation was successful. private bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? reason) { @@ -69,16 +76,62 @@ private bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? reason) return reason != null; } - /// - public string? GetMinText() + /// + /// Get the text to display to the player that represents the difference between their + /// character's age and the upper/lower bound of acceptable ages. + /// + /// A string representing this character's age in relation to the requirement's bounds. + /// A context that may contain this character's profile. + /// Whether or not this operation was successful. + private bool TryGetRangeDifferenceReason([NotNullWhen(true)] out string? reason, + PlayerRequirementContext? context = null) + { + reason = null; + + if (context?.Profile == null) + return false; + + var age = context.Profile.Age; + if (this is IPlayerRangeRequirement range) + reason = range.GetDifferenceReason(age); + + return reason != null; + } + + /// + public string FormatValue(int value) { - return Min?.ToString(); + return value.ToString(); } /// - public string? GetMaxText() + public int? GetDifference(int value) + { + if (this is IPlayerRangeRequirement range && range.IsInRange(value)) + return null; + + // Negative value = greater than maximum + if (Max != null && value > Max) + return Max - value; + + // Positive value = less than minimum + if (Min != null && value < Min) + return Min - value; + + return null; + } + + /// + public int Sign(int difference) + { + return Math.Sign(difference); + } + + /// + public string FormatDifferenceText(int difference) { - return Max?.ToString(); + var diffValue = Math.Abs(difference); + return FormatValue(diffValue); } } @@ -179,7 +232,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) var protoMan = IoCManager.Resolve(); var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan)); var traitList = string.Join(", ", traitNames); - var constraintReason = Count.GetReason(context); + var constraintReason = Count.GetReason(); return Loc.GetString("player-requirement-trait-reason", ("inverted", Inverted), diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl index 64bbd8ba84..fa56fcf911 100644 --- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl @@ -4,7 +4,7 @@ player-requirement-playtime-minmax-time = between {$minimum} and {$maximum} player-requirement-playtime-constraint-reason = Must{$inverted -> [true] {" "}not *[false] {""} -} have {$timeConstraint} of playtime +} have {$constraint} of playtime player-requirement-department-playtime-reason = {$constraint} in the {$department} department. player-requirement-job-playtime-reason = {$constraint} as a {$job}. diff --git a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl index 2a88288b75..eee2764983 100644 --- a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl @@ -2,6 +2,11 @@ player-requirement-range-minimum-reason = at least {$minimum} player-requirement-range-maximum-reason = at most {$maximum} player-requirement-range-minmax-reason = between {$minimum} and {$maximum} +player-requirement-range-with-difference = {$range} {$difference} +player-requirement-range-difference = (Need {$difference}) +player-requirement-range-difference-lower = {$count} more +player-requirement-range-difference-higher = {$count} less + count-requirement-constant-reason = exactly {$count} count-requirement-any-reason = any count-requirement-all-reason = all From 30891b0d995d3241aeee1bd30ed4aa3fb5c1e17a Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:10:38 -0500 Subject: [PATCH 45/54] give jobs/antags timer requirement difference reasons --- .../Players/PlayTimeTracking/JobRequirementsManager.cs | 8 ++++---- .../Players/PlayTimeTracking/JobRequirementManager.cs | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index 94edcc3746..04ac5ddce7 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -158,9 +158,9 @@ public bool IsAllowed( // Begin DEN: Use player requirements var roleSystem = _entManager.System(); var requirements = roleSystem.GetRolePlayerRequirements(job); - if (requirements != null && !PassesRequirements(profile, requirements)) + if (requirements != null && !PassesRequirements(profile, requirements, out var context)) { - reason = SharedPlayerRequirementManager.GetCombinedReason(requirements); + reason = SharedPlayerRequirementManager.GetCombinedReason(requirements, context); return false; } // End DEN @@ -196,9 +196,9 @@ public bool IsAllowed( // Begin DEN: Use player requirements var roleSystem = _entManager.System(); var requirements = roleSystem.GetRolePlayerRequirements(antag); - if (requirements != null && !PassesRequirements(profile, requirements)) + if (requirements != null && !PassesRequirements(profile, requirements, out var context)) { - reason = SharedPlayerRequirementManager.GetCombinedReason(requirements); + reason = SharedPlayerRequirementManager.GetCombinedReason(requirements, context); return false; } // End DEN diff --git a/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs index 9de5ae08af..143ac5d323 100644 --- a/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs +++ b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs @@ -13,12 +13,14 @@ public sealed partial class JobRequirementsManager /// /// A profile associated with the character we're loading in. /// The requirements to check. + /// diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs index b6cd8ed953..ba24121a2e 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs @@ -104,7 +104,8 @@ private bool TryGetRangeDifferenceReason([NotNullWhen(true)] out string? reason, /// public string FormatValue(int value) { - return value.ToString(); + return Loc.GetString("player-requirement-format-number", + ("number", value.ToString())); } /// diff --git a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl index ee4511acc6..388bfd1cf3 100644 --- a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl +++ b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl @@ -1,5 +1,6 @@ player-requirement-format-department = [color={$color}]{$department}[/color] player-requirement-format-job = [color={$color}]{$job}[/color] +player-requirement-format-number = [color=White]{$number}[/color] player-requirement-format-species = [color=Green]{$species}[/color] player-requirement-format-time = [color=Yellow]{$playtime}[/color] player-requirement-format-trait = [color=LightBlue]{$trait}[/color] From 1174b7cad30ed6eb15aa928286120d65a52a9318 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:22:19 -0500 Subject: [PATCH 49/54] refactor player restrictions for antags --- Resources/Prototypes/Roles/Antags/nukeops.yml | 47 ++++++++++++++----- .../Prototypes/Roles/Antags/revolutionary.yml | 11 +++-- Resources/Prototypes/Roles/Antags/thief.yml | 11 +++-- Resources/Prototypes/Roles/Antags/traitor.yml | 22 ++++++--- Resources/Prototypes/Roles/Antags/wizard.yml | 11 +++-- .../Prototypes/Roles/Antags/xenoborgs.yml | 28 +++++++---- Resources/Prototypes/Roles/Antags/zombie.yml | 11 +++-- 7 files changed, 102 insertions(+), 39 deletions(-) diff --git a/Resources/Prototypes/Roles/Antags/nukeops.yml b/Resources/Prototypes/Roles/Antags/nukeops.yml index de86b70560..bf44417685 100644 --- a/Resources/Prototypes/Roles/Antags/nukeops.yml +++ b/Resources/Prototypes/Roles/Antags/nukeops.yml @@ -4,9 +4,14 @@ antagonist: true setPreference: true objective: roles-antag-nuclear-operative-objective - requirements: - - !type:OverallPlaytimeRequirement - time: 5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 5h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 5h + # End DEN guides: [ NuclearOperatives ] - type: antag @@ -16,11 +21,19 @@ setPreference: true objective: roles-antag-nuclear-operative-agent-objective requirements: - - !type:OverallPlaytimeRequirement - time: 5h - - !type:RoleTimeRequirement - role: JobChemist - time: 3h + # Begin DEN: Use PlayerRequirements + # - !type:OverallPlaytimeRequirement + # time: 5h + # - !type:RoleTimeRequirement + # role: JobChemist + # time: 3h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 5h + - !type:PlayerJobPlaytimeRequirement + job: Chemist + minTime: 3h + # End DEN guides: [ NuclearOperatives ] - type: antag @@ -29,13 +42,21 @@ antagonist: true setPreference: true objective: roles-antag-nuclear-operative-commander-objective - requirements: - - !type:OverallPlaytimeRequirement - time: 5h - - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 5h + # - !type:DepartmentTimeRequirement + # department: Security + # time: 5h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 5h + - !type:PlayerDepartmentPlaytimeRequirement department: Security - time: 5h + minTime: 5h # should be changed to nukie playtime when thats tracked (wyci) + # End DEN guides: [ NuclearOperatives ] - type: startingGear diff --git a/Resources/Prototypes/Roles/Antags/revolutionary.yml b/Resources/Prototypes/Roles/Antags/revolutionary.yml index 172876040a..c2f473b8b3 100644 --- a/Resources/Prototypes/Roles/Antags/revolutionary.yml +++ b/Resources/Prototypes/Roles/Antags/revolutionary.yml @@ -5,9 +5,14 @@ setPreference: true objective: roles-antag-rev-head-objective guides: [ Revolutionaries ] - requirements: - - !type:OverallPlaytimeRequirement - time: 1h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 1h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 1h + # End DEN - type: antag id: Rev diff --git a/Resources/Prototypes/Roles/Antags/thief.yml b/Resources/Prototypes/Roles/Antags/thief.yml index 740a7e217f..f3488df5ec 100644 --- a/Resources/Prototypes/Roles/Antags/thief.yml +++ b/Resources/Prototypes/Roles/Antags/thief.yml @@ -5,9 +5,14 @@ setPreference: true objective: roles-antag-thief-objective guides: [ Thieves ] - requirements: - - !type:OverallPlaytimeRequirement - time: 1h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 1h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 1h + # End DEN - type: startingGear id: ThiefGear diff --git a/Resources/Prototypes/Roles/Antags/traitor.yml b/Resources/Prototypes/Roles/Antags/traitor.yml index edc130ef8b..c9da593486 100644 --- a/Resources/Prototypes/Roles/Antags/traitor.yml +++ b/Resources/Prototypes/Roles/Antags/traitor.yml @@ -5,9 +5,14 @@ setPreference: true objective: roles-antag-syndicate-agent-objective guides: [ Traitors ] - requirements: - - !type:OverallPlaytimeRequirement - time: 1h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 1h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 1h + # End DEN - type: antag id: TraitorSleeper @@ -16,9 +21,14 @@ setPreference: true objective: roles-antag-syndicate-agent-sleeper-objective guides: [ Traitors ] - requirements: - - !type:OverallPlaytimeRequirement - time: 1h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 1h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 1h + # End DEN # Syndicate Operative Outfit - Monkey - type: startingGear diff --git a/Resources/Prototypes/Roles/Antags/wizard.yml b/Resources/Prototypes/Roles/Antags/wizard.yml index 9d528f9b79..75145aefdd 100644 --- a/Resources/Prototypes/Roles/Antags/wizard.yml +++ b/Resources/Prototypes/Roles/Antags/wizard.yml @@ -11,9 +11,14 @@ antagonist: true # setPreference: true # Disabled as roundstart gamemode until reworked objective: roles-antag-wizard-objective # TODO: maybe give random objs and stationary ones from AntagObjectives and AntagRandomObjectives - requirements: # I hate time locked roles but this should be enough time for someone to be acclimated - - !type:OverallPlaytimeRequirement - time: 5h + # Begin DEN: Use PlayerRequirements + # requirements: # I hate time locked roles but this should be enough time for someone to be acclimated + # - !type:OverallPlaytimeRequirement + # time: 5h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 5h + # End DEN guides: [ Wizard ] # See wizard_startinggear for wiz start gear options diff --git a/Resources/Prototypes/Roles/Antags/xenoborgs.yml b/Resources/Prototypes/Roles/Antags/xenoborgs.yml index 4e1989be8d..0a8d54e486 100644 --- a/Resources/Prototypes/Roles/Antags/xenoborgs.yml +++ b/Resources/Prototypes/Roles/Antags/xenoborgs.yml @@ -4,10 +4,16 @@ antagonist: true setPreference: true objective: roles-antag-mothership-core-objective - requirements: - - !type:RoleTimeRequirement - role: JobBorg - time: 18000 # 5 hrs + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:RoleTimeRequirement + # role: JobBorg + # time: 18000 # 5 hrs + playerRequirements: + - !type:PlayerJobPlaytimeRequirement + job: JobBorg + minTime: 5h + # End DEN guides: [ Xenoborgs ] - type: antag @@ -16,8 +22,14 @@ antagonist: true setPreference: true objective: roles-antag-xenoborg-objective - requirements: - - !type:RoleTimeRequirement - role: JobBorg - time: 18000 # 5 hrs + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:RoleTimeRequirement + # role: JobBorg + # time: 18000 # 5 hrs + playerRequirements: + - !type:PlayerJobPlaytimeRequirement + job: JobBorg + minTime: 5h + # End DEN guides: [ Xenoborgs ] diff --git a/Resources/Prototypes/Roles/Antags/zombie.yml b/Resources/Prototypes/Roles/Antags/zombie.yml index fa6561aa5e..2c5fa08b3e 100644 --- a/Resources/Prototypes/Roles/Antags/zombie.yml +++ b/Resources/Prototypes/Roles/Antags/zombie.yml @@ -5,9 +5,14 @@ setPreference: true objective: roles-antag-initial-infected-objective guides: [ Zombies ] - requirements: - - !type:OverallPlaytimeRequirement - time: 1h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 1h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 1h + # End DEN - type: antag id: Zombie From ce40ea33a61afe98a5dbe9fad7239ebe01c3a9fa Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:12:10 -0500 Subject: [PATCH 50/54] migrate jobs to using playerrequirements --- .../Roles/Jobs/Cargo/quartermaster.yml | 32 ++++++++---- .../Roles/Jobs/Cargo/salvage_specialist.yml | 14 ++++-- .../Roles/Jobs/Civilian/bartender.yml | 14 ++++-- .../Prototypes/Roles/Jobs/Civilian/chef.yml | 14 ++++-- .../Prototypes/Roles/Jobs/Civilian/lawyer.yml | 11 ++-- .../Prototypes/Roles/Jobs/Civilian/mime.yml | 11 ++-- .../Roles/Jobs/Civilian/service_worker.yml | 12 +++-- .../Prototypes/Roles/Jobs/Command/captain.yml | 50 +++++++++++++------ .../Roles/Jobs/Command/head_of_personnel.yml | 50 +++++++++++++------ .../Engineering/atmospheric_technician.yml | 12 +++-- .../Roles/Jobs/Engineering/chief_engineer.yml | 32 ++++++++---- .../Jobs/Engineering/station_engineer.yml | 14 ++++-- .../Jobs/Engineering/technical_assistant.yml | 22 +++++--- .../Prototypes/Roles/Jobs/Medical/chemist.yml | 14 ++++-- .../Jobs/Medical/chief_medical_officer.yml | 32 ++++++++---- .../Roles/Jobs/Medical/medical_doctor.yml | 14 ++++-- .../Roles/Jobs/Medical/medical_intern.yml | 16 ++++-- .../Roles/Jobs/Medical/paramedic.yml | 12 +++-- .../Prototypes/Roles/Jobs/Science/borg.yml | 25 +++++++--- .../Roles/Jobs/Science/research_assistant.yml | 16 ++++-- .../Roles/Jobs/Science/research_director.yml | 21 +++++--- .../Roles/Jobs/Science/scientist.yml | 14 ++++-- .../Roles/Jobs/Security/detective.yml | 14 ++++-- .../Roles/Jobs/Security/head_of_security.yml | 41 ++++++++++----- .../Roles/Jobs/Security/security_cadet.yml | 22 +++++--- .../Roles/Jobs/Security/security_officer.yml | 14 ++++-- .../Prototypes/Roles/Jobs/Security/warden.yml | 23 ++++++--- .../Roles/requirement_overrides.yml | 43 ++++++++++------ 28 files changed, 425 insertions(+), 184 deletions(-) diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml index 92b9b93671..e8bc709ed3 100644 --- a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml +++ b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml @@ -3,16 +3,28 @@ name: job-name-qm description: job-description-qm playTimeTracker: JobQuartermaster - requirements: - - !type:RoleTimeRequirement - role: JobCargoTechnician - time: 5h - - !type:RoleTimeRequirement - role: JobSalvageSpecialist - time: 2.5h - - !type:DepartmentTimeRequirement - department: Cargo - time: 10h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:RoleTimeRequirement + # role: JobCargoTechnician + # time: 5h + # - !type:RoleTimeRequirement + # role: JobSalvageSpecialist + # time: 2.5h + # - !type:DepartmentTimeRequirement + # department: Cargo + # time: 10h + playerRequirements: + - !type:PlayerJobPlaytimeRequirement + job: CargoTechnician + minTime: 5h + - !type:PlayerJobPlaytimeRequirement + job: SalvageSpecialist + minTime: 2.5h + - !type:PlayerDepartmentPlaytimeRequirement + department: Cargo + minTime: 10h + # End DEN weight: 10 startingGear: QuartermasterGear icon: "JobIconQuarterMaster" diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml b/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml index 7756f8dde9..57197e2f32 100644 --- a/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml +++ b/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml @@ -3,10 +3,16 @@ name: job-name-salvagespec description: job-description-salvagespec playTimeTracker: JobSalvageSpecialist - requirements: - - !type:DepartmentTimeRequirement - department: Cargo - time: 2.5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Cargo + # time: 2.5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Cargo + minTime: 2.5h + # End DEN icon: "JobIconShaftMiner" startingGear: SalvageSpecialistGear supervisors: job-supervisors-qm diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml index 2d32b87f68..24a00314f7 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml @@ -3,10 +3,16 @@ name: job-name-bartender description: job-description-bartender playTimeTracker: JobBartender - requirements: - - !type:DepartmentTimeRequirement - department: Civilian - time: 0.5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Civilian + # time: 0.5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Civilian + minTime: 0.5h + # End DEN startingGear: BartenderGear icon: "JobIconBartender" supervisors: job-supervisors-hop diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml index b3a5deab15..ff2325e06e 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml @@ -3,10 +3,16 @@ name: job-name-chef description: job-description-chef playTimeTracker: JobChef - requirements: - - !type:DepartmentTimeRequirement - department: Civilian - time: 0.5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Civilian + # time: 0.5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Civilian + minTime: 0.5h + # End DEN startingGear: ChefGear icon: "JobIconChef" supervisors: job-supervisors-hop diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml b/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml index 11f07252a3..26dee4de45 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml @@ -3,9 +3,14 @@ name: job-name-lawyer description: job-description-lawyer playTimeTracker: JobLawyer - requirements: - - !type:OverallPlaytimeRequirement - time: 2.5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 2.5h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 2.5h + # End DEN startingGear: LawyerGear icon: "JobIconLawyer" supervisors: job-supervisors-hop diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml index 8ff045173d..88ec64d7b6 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml @@ -3,9 +3,14 @@ name: job-name-mime description: job-description-mime playTimeTracker: JobMime - requirements: - - !type:OverallPlaytimeRequirement - time: 4h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 4h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 4h + # End DEN startingGear: MimeGear icon: "JobIconMime" supervisors: job-supervisors-hop diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml b/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml index 0c83971827..a04528f220 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml @@ -3,10 +3,16 @@ name: job-name-serviceworker description: job-description-serviceworker playTimeTracker: JobServiceWorker - requirements: - - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Civilian + # time: 0.5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement department: Civilian - time: 0.5h + minTime: 0.5h + # End DEN startingGear: ServiceWorkerGear icon: "JobIconServiceWorker" supervisors: job-supervisors-service diff --git a/Resources/Prototypes/Roles/Jobs/Command/captain.yml b/Resources/Prototypes/Roles/Jobs/Command/captain.yml index 54251b263b..63fb5803d1 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/captain.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/captain.yml @@ -3,22 +3,40 @@ name: job-name-captain description: job-description-captain playTimeTracker: JobCaptain - requirements: - - !type:DepartmentTimeRequirement - department: Engineering - time: 4h - - !type:DepartmentTimeRequirement - department: Medical - time: 4h - - !type:DepartmentTimeRequirement - department: Science - time: 4h - - !type:DepartmentTimeRequirement - department: Security - time: 4h - - !type:DepartmentTimeRequirement - department: Command - time: 4h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Engineering + # time: 4h + # - !type:DepartmentTimeRequirement + # department: Medical + # time: 4h + # - !type:DepartmentTimeRequirement + # department: Science + # time: 4h + # - !type:DepartmentTimeRequirement + # department: Security + # time: 4h + # - !type:DepartmentTimeRequirement + # department: Command + # time: 4h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Engineering + minTime: 4h + - !type:PlayerDepartmentPlaytimeRequirement + department: Medical + minTime: 4h + - !type:PlayerDepartmentPlaytimeRequirement + department: Science + minTime: 4h + - !type:PlayerDepartmentPlaytimeRequirement + department: Security + minTime: 4h + - !type:PlayerDepartmentPlaytimeRequirement + department: Command + minTime: 4h + # End DEN weight: 20 startingGear: CaptainGear icon: "JobIconCaptain" diff --git a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml index 9150ca39f3..56276d2798 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml @@ -3,22 +3,40 @@ name: job-name-hop description: job-description-hop playTimeTracker: JobHeadOfPersonnel - requirements: - - !type:DepartmentTimeRequirement - department: Engineering - time: 2.5h - - !type:DepartmentTimeRequirement - department: Medical - time: 2.5h - - !type:DepartmentTimeRequirement - department: Science - time: 2.5h - - !type:DepartmentTimeRequirement - department: Security - time: 2.5h - - !type:DepartmentTimeRequirement - department: Command - time: 2.5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Engineering + # time: 2.5h + # - !type:DepartmentTimeRequirement + # department: Medical + # time: 2.5h + # - !type:DepartmentTimeRequirement + # department: Science + # time: 2.5h + # - !type:DepartmentTimeRequirement + # department: Security + # time: 2.5h + # - !type:DepartmentTimeRequirement + # department: Command + # time: 2.5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Engineering + minTime: 2.5h + - !type:PlayerDepartmentPlaytimeRequirement + department: Medical + minTime: 2.5h + - !type:PlayerDepartmentPlaytimeRequirement + department: Science + minTime: 2.5h + - !type:PlayerDepartmentPlaytimeRequirement + department: Security + minTime: 2.5h + - !type:PlayerDepartmentPlaytimeRequirement + department: Command + minTime: 2.5h + # End DEN weight: 20 startingGear: HoPGear icon: "JobIconHeadOfPersonnel" diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml b/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml index 20a6dc7ace..ba35f9f5e7 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml @@ -3,10 +3,16 @@ name: job-name-atmostech description: job-description-atmostech playTimeTracker: JobAtmosphericTechnician - requirements: - - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Engineering + # time: 2.5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement department: Engineering - time: 2.5h + minTime: 2.5h + # End DEN startingGear: AtmosphericTechnicianGear icon: "JobIconAtmosphericTechnician" supervisors: job-supervisors-ce diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml index 561d026020..fe55c00411 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml @@ -3,16 +3,28 @@ name: job-name-ce description: job-description-ce playTimeTracker: JobChiefEngineer - requirements: - - !type:RoleTimeRequirement - role: JobAtmosphericTechnician - time: 2.5h - - !type:RoleTimeRequirement - role: JobStationEngineer - time: 5h - - !type:DepartmentTimeRequirement - department: Engineering - time: 10h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:RoleTimeRequirement + # role: JobAtmosphericTechnician + # time: 2.5h + # - !type:RoleTimeRequirement + # role: JobStationEngineer + # time: 5h + # - !type:DepartmentTimeRequirement + # department: Engineering + # time: 10h + playerRequirements: + - !type:PlayerJobPlaytimeRequirement + job: AtmosphericTechnician + minTime: 2.5h + - !type:PlayerJobPlaytimeRequirement + job: StationEngineer + minTime: 5h + - !type:PlayerDepartmentPlaytimeRequirement + department: Engineering + minTime: 10h + # End DEN weight: 10 startingGear: ChiefEngineerGear icon: "JobIconChiefEngineer" diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml index c99140b095..ca10c9b1f6 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml @@ -3,10 +3,16 @@ name: job-name-engineer description: job-description-engineer playTimeTracker: JobStationEngineer - requirements: - - !type:DepartmentTimeRequirement - department: Engineering - time: 2.5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Engineering + # time: 2.5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Engineering + minTime: 2.5h + # End DEN startingGear: StationEngineerGear icon: "JobIconStationEngineer" supervisors: job-supervisors-ce diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml b/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml index ee4431bded..257199a3d2 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml @@ -3,13 +3,21 @@ name: job-name-technical-assistant description: job-description-technical-assistant playTimeTracker: JobTechnicalAssistant - requirements: - - !type:OverallPlaytimeRequirement - time: 1h - - !type:DepartmentTimeRequirement - department: Engineering - time: 10h - inverted: true # stop playing intern if you're good at engineering! + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 1h + # - !type:DepartmentTimeRequirement + # department: Engineering + # time: 10h + # inverted: true # stop playing intern if you're good at engineering! + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 1h + - !type:PlayerDepartmentPlaytimeRequirement + department: Engineering + maxTime: 10h + # End DEN startingGear: TechnicalAssistantGear icon: "JobIconTechnicalAssistant" supervisors: job-supervisors-engineering diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml b/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml index 6f86179015..b916c116f3 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml @@ -3,10 +3,16 @@ name: job-name-chemist description: job-description-chemist playTimeTracker: JobChemist - requirements: - - !type:DepartmentTimeRequirement - department: Medical - time: 5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Medical + # time: 5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Medical + minTime: 5h + # End DEN startingGear: ChemistGear icon: "JobIconChemist" supervisors: job-supervisors-cmo diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml index 5abd370503..48ff1e6814 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml @@ -5,16 +5,28 @@ name: job-name-cmo description: job-description-cmo playTimeTracker: JobChiefMedicalOfficer - requirements: - - !type:RoleTimeRequirement - role: JobChemist - time: 2.5h - - !type:RoleTimeRequirement - role: JobMedicalDoctor - time: 5h - - !type:DepartmentTimeRequirement - department: Medical - time: 10h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:RoleTimeRequirement + # role: JobChemist + # time: 2.5h + # - !type:RoleTimeRequirement + # role: JobMedicalDoctor + # time: 5h + # - !type:DepartmentTimeRequirement + # department: Medical + # time: 10h + playerRequirements: + - !type:PlayerJobPlaytimeRequirement + job: Chemist + minTime: 2.5h + - !type:PlayerJobPlaytimeRequirement + job: MedicalDoctor + minTime: 5h + - !type:PlayerDepartmentPlaytimeRequirement + department: Medical + minTime: 10h + # End DEN weight: 10 startingGear: CMOGear icon: "JobIconChiefMedicalOfficer" diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml index f012f25197..9895710266 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml @@ -3,10 +3,16 @@ name: job-name-doctor description: job-description-doctor playTimeTracker: JobMedicalDoctor - requirements: - - !type:DepartmentTimeRequirement - department: Medical - time: 2.5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Medical + # time: 2.5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Medical + minTime: 2.5h + # End DEN startingGear: DoctorGear icon: "JobIconMedicalDoctor" supervisors: job-supervisors-cmo diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml index cc96b6bee2..c29397c3d8 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml @@ -3,11 +3,17 @@ name: job-name-intern description: job-description-intern playTimeTracker: JobMedicalIntern - requirements: - - !type:DepartmentTimeRequirement - department: Medical - time: 10h - inverted: true # stop playing intern if you're good at med! + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Medical + # time: 10h + # inverted: true # stop playing intern if you're good at med! + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Medical + maxTime: 10h + # End DEN startingGear: MedicalInternGear icon: "JobIconMedicalIntern" supervisors: job-supervisors-medicine diff --git a/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml b/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml index 4ef5f08e4d..10f33cf27e 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml @@ -3,10 +3,16 @@ name: job-name-paramedic description: job-description-paramedic playTimeTracker: JobParamedic - requirements: - - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Medical + # time: 2.5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement department: Medical - time: 2.5h + minTime: 2.5h + # End DEN startingGear: ParamedicGear icon: "JobIconParamedic" supervisors: job-supervisors-cmo diff --git a/Resources/Prototypes/Roles/Jobs/Science/borg.yml b/Resources/Prototypes/Roles/Jobs/Science/borg.yml index 7834043799..adad522466 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/borg.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/borg.yml @@ -5,10 +5,16 @@ name: job-name-station-ai description: job-description-station-ai playTimeTracker: JobStationAi - requirements: - - !type:RoleTimeRequirement - role: JobBorg - time: 5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:RoleTimeRequirement + # role: JobBorg + # time: 5h + playerRequirements: + - !type:PlayerJobPlaytimeRequirement + job: JobBorg + minTime: 5h + # End DEN weight: 10 canBeAntag: false icon: JobIconStationAi @@ -22,9 +28,14 @@ name: job-name-borg description: job-description-borg playTimeTracker: JobBorg - requirements: - - !type:OverallPlaytimeRequirement - time: 10h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 10h + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 10h + # End DEN canBeAntag: false icon: JobIconBorg supervisors: job-supervisors-rd diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml b/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml index 4805985822..70c8e41b52 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml @@ -3,11 +3,17 @@ name: job-name-research-assistant description: job-description-research-assistant playTimeTracker: JobResearchAssistant - requirements: - - !type:DepartmentTimeRequirement - department: Science - time: 10h - inverted: true # stop playing intern if you're good at science! + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Science + # time: 10h + # inverted: true # stop playing intern if you're good at science! + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Science + maxTime: 10h + # End DEN startingGear: ResearchAssistantGear icon: "JobIconResearchAssistant" supervisors: job-supervisors-science diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml index a7b057dc1b..6e1d67ec37 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml @@ -3,13 +3,22 @@ name: job-name-rd description: job-description-rd playTimeTracker: JobResearchDirector - requirements: - - !type:RoleTimeRequirement - role: JobScientist - time: 5h - - !type:DepartmentTimeRequirement + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:RoleTimeRequirement + # role: JobScientist + # time: 5h + # - !type:DepartmentTimeRequirement + # department: Science + # time: 10h + playerRequirements: + - !type:PlayerJobPlaytimeRequirement + job: Scientist + minTime: 5h + - !type:PlayerDepartmentPlaytimeRequirement department: Science - time: 10h + minTime: 10h + # End DEN weight: 10 startingGear: ResearchDirectorGear icon: "JobIconResearchDirector" diff --git a/Resources/Prototypes/Roles/Jobs/Science/scientist.yml b/Resources/Prototypes/Roles/Jobs/Science/scientist.yml index 5e8e1b6e14..c1a7b6d927 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/scientist.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/scientist.yml @@ -3,10 +3,16 @@ name: job-name-scientist description: job-description-scientist playTimeTracker: JobScientist - requirements: - - !type:DepartmentTimeRequirement - department: Science - time: 2.5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Science + # time: 2.5h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Science + minTime: 2.5h + # End DEN startingGear: ScientistGear icon: "JobIconScientist" supervisors: job-supervisors-rd diff --git a/Resources/Prototypes/Roles/Jobs/Security/detective.yml b/Resources/Prototypes/Roles/Jobs/Security/detective.yml index b38f2c8b1c..bff111939b 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/detective.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/detective.yml @@ -3,10 +3,16 @@ name: job-name-detective description: job-description-detective playTimeTracker: JobDetective - requirements: - - !type:RoleTimeRequirement - role: JobSecurityOfficer - time: 5h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:RoleTimeRequirement + # role: JobSecurityOfficer + # time: 5h + playerRequirements: + - !type:PlayerJobPlaytimeRequirement + job: SecurityOfficer + minTime: 5h + # End DEN startingGear: DetectiveGear icon: "JobIconDetective" supervisors: job-supervisors-hos diff --git a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml index e8fdc6f738..daf6b0dff0 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml @@ -3,19 +3,34 @@ name: job-name-hos description: job-description-hos playTimeTracker: JobHeadOfSecurity - requirements: - - !type:RoleTimeRequirement - role: JobWarden - time: 3h - - !type:RoleTimeRequirement - role: JobDetective - time: 1h # knowing how to use the tools is important - - !type:RoleTimeRequirement - role: JobSecurityOfficer - time: 5h - - !type:DepartmentTimeRequirement - department: Security - time: 10h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:RoleTimeRequirement + # role: JobWarden + # time: 3h + # - !type:RoleTimeRequirement + # role: JobDetective + # time: 1h # knowing how to use the tools is important + # - !type:RoleTimeRequirement + # role: JobSecurityOfficer + # time: 5h + # - !type:DepartmentTimeRequirement + # department: Security + # time: 10h + playerRequirements: + - !type:PlayerJobPlaytimeRequirement + job: Detective + minTime: 1h + - !type:PlayerJobPlaytimeRequirement + job: SecurityOfficer + minTime: 5h + - !type:PlayerJobPlaytimeRequirement + job: Warden + minTime: 3h + - !type:PlayerDepartmentPlaytimeRequirement + department: Security + minTime: 10h + # End DEN weight: 10 startingGear: HoSGear icon: "JobIconHeadOfSecurity" diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml b/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml index 47b7472541..f6e8b0ce15 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml @@ -3,13 +3,21 @@ name: job-name-cadet description: job-description-cadet playTimeTracker: JobSecurityCadet - requirements: - - !type:OverallPlaytimeRequirement - time: 10h - - !type:DepartmentTimeRequirement - department: Security - time: 10h - inverted: true # stop playing intern if you're good at security! + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:OverallPlaytimeRequirement + # time: 10h + # - !type:DepartmentTimeRequirement + # department: Security + # time: 10h + # inverted: true # stop playing intern if you're good at security! + playerRequirements: + - !type:PlayerOverallPlaytimeRequirement + minTime: 10h + - !type:PlayerDepartmentPlaytimeRequirement + department: Security + maxTime: 10h + # End DEN startingGear: SecurityCadetGear icon: "JobIconSecurityCadet" supervisors: job-supervisors-security diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml index af8a274d77..627588e21d 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml @@ -3,10 +3,16 @@ name: job-name-security description: job-description-security playTimeTracker: JobSecurityOfficer - requirements: - - !type:DepartmentTimeRequirement - department: Security - time: 4h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:DepartmentTimeRequirement + # department: Security + # time: 4h + playerRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Security + minTime: 4h + # End DEN startingGear: SecurityOfficerGear icon: "JobIconSecurityOfficer" supervisors: job-supervisors-hos diff --git a/Resources/Prototypes/Roles/Jobs/Security/warden.yml b/Resources/Prototypes/Roles/Jobs/Security/warden.yml index 2d8813a69e..2fcb39047a 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/warden.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/warden.yml @@ -3,13 +3,22 @@ name: job-name-warden description: job-description-warden playTimeTracker: JobWarden - requirements: - - !type:RoleTimeRequirement - role: JobSecurityOfficer - time: 5h - - !type:DepartmentTimeRequirement - department: Security - time: 10h + # Begin DEN: Use PlayerRequirements + # requirements: + # - !type:RoleTimeRequirement + # role: JobSecurityOfficer + # time: 5h + # - !type:DepartmentTimeRequirement + # department: Security + # time: 10h + playerRequirements: + - !type:PlayerJobPlaytimeRequirement + job: SecurityOfficer + minTime: 5h + - !type:PlayerDepartmentPlaytimeRequirement + department: Security + minTime: 10h + # End DEN weight: 5 startingGear: WardenGear icon: "JobIconWarden" diff --git a/Resources/Prototypes/Roles/requirement_overrides.yml b/Resources/Prototypes/Roles/requirement_overrides.yml index 752249e90b..85b3651588 100644 --- a/Resources/Prototypes/Roles/requirement_overrides.yml +++ b/Resources/Prototypes/Roles/requirement_overrides.yml @@ -1,16 +1,31 @@ - type: jobRequirementOverride id: Reduced - jobs: - Captain: - - !type:DepartmentTimeRequirement - department: Engineering - time: 1h - - !type:DepartmentTimeRequirement - department: Medical - time: 1h - - !type:DepartmentTimeRequirement - department: Security - time: 1h - - !type:DepartmentTimeRequirement - department: Command - time: 1h + # Begin DEN: Use PlayerRequirements + # jobs: + # Captain: + # - !type:DepartmentTimeRequirement + # department: Engineering + # time: 1h + # - !type:DepartmentTimeRequirement + # department: Medical + # time: 1h + # - !type:DepartmentTimeRequirement + # department: Security + # time: 1h + # - !type:DepartmentTimeRequirement + # department: Command + # time: 1h + jobRequirements: + - !type:PlayerDepartmentPlaytimeRequirement + department: Engineering + minTime: 1h + - !type:PlayerDepartmentPlaytimeRequirement + department: Medical + minTime: 1h + - !type:PlayerDepartmentPlaytimeRequirement + department: Security + minTime: 1h + - !type:PlayerDepartmentPlaytimeRequirement + department: Command + minTime: 1h + # End DEN From 57d93182a4d94a3f987ab94ad2c651c2e712423f Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:22:20 -0500 Subject: [PATCH 51/54] whoops i forgot to put this under a Captain key --- .../Roles/requirement_overrides.yml | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Resources/Prototypes/Roles/requirement_overrides.yml b/Resources/Prototypes/Roles/requirement_overrides.yml index 85b3651588..e7904921bc 100644 --- a/Resources/Prototypes/Roles/requirement_overrides.yml +++ b/Resources/Prototypes/Roles/requirement_overrides.yml @@ -16,16 +16,17 @@ # department: Command # time: 1h jobRequirements: - - !type:PlayerDepartmentPlaytimeRequirement - department: Engineering - minTime: 1h - - !type:PlayerDepartmentPlaytimeRequirement - department: Medical - minTime: 1h - - !type:PlayerDepartmentPlaytimeRequirement - department: Security - minTime: 1h - - !type:PlayerDepartmentPlaytimeRequirement - department: Command - minTime: 1h + Captain: + - !type:PlayerDepartmentPlaytimeRequirement + department: Engineering + minTime: 1h + - !type:PlayerDepartmentPlaytimeRequirement + department: Medical + minTime: 1h + - !type:PlayerDepartmentPlaytimeRequirement + department: Security + minTime: 1h + - !type:PlayerDepartmentPlaytimeRequirement + department: Command + minTime: 1h # End DEN From 24275564bbea43c0049d2a5c4d156dfe10287d7f Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:25:44 -0500 Subject: [PATCH 52/54] fix borg job IDs oops --- Resources/Prototypes/Roles/Antags/xenoborgs.yml | 4 ++-- Resources/Prototypes/Roles/Jobs/Science/borg.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/Prototypes/Roles/Antags/xenoborgs.yml b/Resources/Prototypes/Roles/Antags/xenoborgs.yml index 0a8d54e486..c3c53f4dee 100644 --- a/Resources/Prototypes/Roles/Antags/xenoborgs.yml +++ b/Resources/Prototypes/Roles/Antags/xenoborgs.yml @@ -11,7 +11,7 @@ # time: 18000 # 5 hrs playerRequirements: - !type:PlayerJobPlaytimeRequirement - job: JobBorg + job: Borg minTime: 5h # End DEN guides: [ Xenoborgs ] @@ -29,7 +29,7 @@ # time: 18000 # 5 hrs playerRequirements: - !type:PlayerJobPlaytimeRequirement - job: JobBorg + job: Borg minTime: 5h # End DEN guides: [ Xenoborgs ] diff --git a/Resources/Prototypes/Roles/Jobs/Science/borg.yml b/Resources/Prototypes/Roles/Jobs/Science/borg.yml index adad522466..3aa5cf483a 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/borg.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/borg.yml @@ -12,7 +12,7 @@ # time: 5h playerRequirements: - !type:PlayerJobPlaytimeRequirement - job: JobBorg + job: Borg minTime: 5h # End DEN weight: 10 From c41e950dbc6dde80f5b75971897bf370eaadf210 Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:42:33 -0500 Subject: [PATCH 53/54] add errors for invalid ids --- .../PlayerRequirements/PlayerRequirements.Playtime.cs | 8 ++++---- .../PlayerRequirements/PlayerRequirements.Profile.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs index 9a178972ad..0dc1326ae5 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs @@ -266,7 +266,7 @@ protected override bool TryGetRangeConstraintReason([NotNullWhen(true)] out stri /// The department name of this prototype, formatted. private string FormatDepartment(IPrototypeManager protoMan) { - if (!protoMan.TryIndex(Department, out var department)) + if (!protoMan.Resolve(Department, out var department)) return Department; var deptName = Loc.GetString(department.Name); @@ -289,7 +289,7 @@ private string FormatDepartment(IPrototypeManager protoMan) var playtime = TimeSpan.Zero; if (context.Playtimes == null - || !protoMan.TryIndex(Department, out var department)) + || !protoMan.Resolve(Department, out var department)) return null; // Sum the playtimes of all roles in this department. @@ -349,7 +349,7 @@ protected override bool TryGetRangeConstraintReason([NotNullWhen(true)] out stri /// The department name of this prototype, formatted. private string FormatJob(IPrototypeManager protoMan) { - if (!protoMan.TryIndex(Job, out var job)) + if (!protoMan.Resolve(Job, out var job)) return Job; var jobName = Loc.GetString(job.Name); @@ -378,7 +378,7 @@ private string FormatJob(IPrototypeManager protoMan) var protoMan = IoCManager.Resolve(); var playtime = TimeSpan.Zero; - if (context.Playtimes == null || !protoMan.TryIndex(Job, out var job)) + if (context.Playtimes == null || !protoMan.Resolve(Job, out var job)) return null; if (context.Playtimes.TryGetValue(job.PlayTimeTracker, out var tracker)) diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs index ba24121a2e..d388826c07 100644 --- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs +++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs @@ -192,7 +192,7 @@ public override bool CheckRequirement(PlayerRequirementContext context) private static string LocalizeSpecies(ProtoId speciesId, IPrototypeManager protoMan) { var speciesName = speciesId; - if (!protoMan.TryIndex(speciesId, out var species)) + if (!protoMan.Resolve(speciesId, out var species)) return speciesName; speciesName = Loc.GetString(species.Name); @@ -254,7 +254,7 @@ private static string LocalizeTrait(ProtoId traitId, IProt { var traitName = traitId; - if (protoMan.TryIndex(traitId, out var trait)) + if (protoMan.Resolve(traitId, out var trait)) traitName = Loc.GetString(trait.Name); return Loc.GetString("player-requirement-format-trait", ("trait", traitName)); From 976dc5ad44ac6e8cfd687a8dcfdfa17d77c7246d Mon Sep 17 00:00:00 2001 From: portfiend <109661617+portfiend@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:54:03 -0500 Subject: [PATCH 54/54] fixes --- .../Loadouts/Effects/PlayerRequirementLoadoutEffect.cs | 2 +- Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml | 2 +- .../Loadouts/Jobs/Engineering/station_engineer.yml | 2 +- Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml | 4 ++-- Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml | 5 +++++ 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs index e5d4e7b159..ef782f3bdb 100644 --- a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs +++ b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs @@ -28,7 +28,7 @@ public override bool Validate(HumanoidCharacterProfile profile, || Requirement is PlayerPlaytimeRequirement playtimeReq && playtimeReq.ShouldAutoPass()) { reason = FormattedMessage.Empty; - return false; + return true; } var requirements = collection.Resolve(); diff --git a/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml index 1b7c4bf524..ecd48c1153 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml @@ -5,7 +5,7 @@ # Begin DEN: Use PlayerRequirements - !type:PlayerRequirementLoadoutEffect requirement: !type:PlayerJobPlaytimeRequirement - job: QuarterMaster + job: Quartermaster minTime: 20h requirementType: Loadout # - !type:JobRequirementLoadoutEffect diff --git a/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml index 9c018b2725..32083b4e8e 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml @@ -5,7 +5,7 @@ # Begin DEN: Use PlayerRequirements - !type:PlayerRequirementLoadoutEffect requirement: !type:PlayerJobPlaytimeRequirement - job: AtmosphericsTechnician + job: AtmosphericTechnician minTime: 6h requirementType: Loadout - !type:PlayerRequirementLoadoutEffect diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml b/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml index ed80248fb7..ceaaf4906b 100644 --- a/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml +++ b/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml @@ -20,8 +20,8 @@ effects: # Begin DEN: Use PlayerRequirements - !type:PlayerRequirementLoadoutEffect - requirement: !type:PlayerJobPlaytimeRequirement - job: Cargo + requirement: !type:PlayerDepartmentPlaytimeRequirement + department: Cargo minTime: 10h requirementType: Loadout # - !type:JobRequirementLoadoutEffect diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml b/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml index ccb084300f..aba71486bc 100644 --- a/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml +++ b/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml @@ -7,6 +7,7 @@ requirement: !type:PlayerDepartmentPlaytimeRequirement department: Command minTime: 1h + requirementType: Loadout # - !type:JobRequirementLoadoutEffect # requirement: # !type:DepartmentTimeRequirement @@ -288,6 +289,7 @@ requirement: !type:PlayerJobPlaytimeRequirement job: Clown minTime: 1h + requirementType: Loadout # - !type:JobRequirementLoadoutEffect # requirement: # !type:RoleTimeRequirement @@ -307,6 +309,7 @@ requirement: !type:PlayerJobPlaytimeRequirement job: Mime minTime: 1h + requirementType: Loadout # - !type:JobRequirementLoadoutEffect # requirement: # !type:RoleTimeRequirement @@ -326,6 +329,7 @@ requirement: !type:PlayerDepartmentPlaytimeRequirement department: Command minTime: 5h + requirementType: Loadout # - !type:JobRequirementLoadoutEffect # requirement: # !type:DepartmentTimeRequirement @@ -345,6 +349,7 @@ requirement: !type:PlayerJobPlaytimeRequirement job: Librarian minTime: 1h + requirementType: Loadout # - !type:JobRequirementLoadoutEffect # requirement: # !type:RoleTimeRequirement