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/Managers/PlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
new file mode 100644
index 0000000000..8eafb3d728
--- /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 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;
+ }
+}
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);
+}
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.