diff --git a/Content.Client/LateJoin/LateJoinGui.cs b/Content.Client/LateJoin/LateJoinGui.cs index 62a06629f2..13cf281513 100644 --- a/Content.Client/LateJoin/LateJoinGui.cs +++ b/Content.Client/LateJoin/LateJoinGui.cs @@ -2,9 +2,11 @@ using System.Linq; using System.Numerics; using Content.Client.CrewManifest; using Content.Client.GameTicking.Managers; +using Content.Client.Lobby; using Content.Client.UserInterface.Controls; using Content.Client.Players.PlayTimeTracking; using Content.Shared.CCVar; +using Content.Shared.Preferences; using Content.Shared.Roles; using Content.Shared.StatusIcon; using Robust.Client.Console; @@ -26,6 +28,7 @@ namespace Content.Client.LateJoin [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystem = default!; [Dependency] private readonly JobRequirementsManager _jobRequirements = default!; + [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!; public event Action<(NetEntity, string)> SelectedId; @@ -254,7 +257,7 @@ namespace Content.Client.LateJoin jobButton.OnPressed += _ => SelectedId.Invoke((id, jobButton.JobId)); - if (!_jobRequirements.IsAllowed(prototype, out var reason)) + if (!_jobRequirements.IsAllowed(prototype, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason)) { jobButton.Disabled = true; diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index 1509a2fed1..c5f2f311d9 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -649,7 +649,7 @@ namespace Content.Client.Lobby.UI selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1); var requirements = _entManager.System().GetAntagRequirement(antag); - if (!_requirements.CheckRoleTime(requirements, out var reason)) + if (!_requirements.CheckRoleRequirements(requirements, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason)) { selector.LockRequirements(reason); Profile = Profile?.WithAntagPreference(antag.ID, false); @@ -903,7 +903,7 @@ namespace Content.Client.Lobby.UI icon.Texture = jobIcon.Icon.Frame0(); selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon, job.Guides); - if (!_requirements.IsAllowed(job, out var reason)) + if (!_requirements.IsAllowed(job, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason)) { selector.LockRequirements(reason); } diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index bd4ac877db..8ce22489c7 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -1,8 +1,10 @@ using System.Diagnostics.CodeAnalysis; +using Content.Client.Lobby; using Content.Shared.CCVar; using Content.Shared.Players; using Content.Shared.Players.JobWhitelist; using Content.Shared.Players.PlayTimeTracking; +using Content.Shared.Preferences; using Content.Shared.Roles; using Robust.Client; using Robust.Client.Player; @@ -89,7 +91,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager Updated?.Invoke(); } - public bool IsAllowed(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason) + public bool IsAllowed(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason) { reason = null; @@ -106,16 +108,16 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager if (player == null) return true; - return CheckRoleTime(job, out reason); + return CheckRoleRequirements(job, profile, out reason); } - public bool CheckRoleTime(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason) + public bool CheckRoleRequirements(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason) { var reqs = _entManager.System().GetJobRequirement(job); - return CheckRoleTime(reqs, out reason); + return CheckRoleRequirements(reqs, profile, out reason); } - public bool CheckRoleTime(HashSet? requirements, [NotNullWhen(false)] out FormattedMessage? reason) + public bool CheckRoleRequirements(HashSet? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason) { reason = null; @@ -125,7 +127,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager var reasons = new List(); foreach (var requirement in requirements) { - if (JobRequirements.TryRequirementMet(requirement, _roles, out var jobReason, _entManager, _prototypes)) + if (requirement.Check(_entManager, _prototypes, profile, _roles, out var jobReason)) continue; reasons.Add(jobReason.ToMarkup()); diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEui.cs b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEui.cs index 33358a68a4..6b183362e5 100644 --- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEui.cs +++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEui.cs @@ -93,7 +93,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles bool hasAccess = true; FormattedMessage? reason; - if (!requirementsManager.CheckRoleTime(group.Key.Requirements, out reason)) + if (!requirementsManager.CheckRoleRequirements(group.Key.Requirements, null, out reason)) { hasAccess = false; } diff --git a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs index ea6f0ad3f4..4139499e9f 100644 --- a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs +++ b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs @@ -6,6 +6,7 @@ using Content.Server.Afk.Events; using Content.Server.GameTicking; using Content.Server.GameTicking.Events; using Content.Server.Mind; +using Content.Server.Preferences.Managers; using Content.Server.Station.Events; using Content.Shared.CCVar; using Content.Shared.GameTicking; @@ -13,6 +14,7 @@ using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Players; using Content.Shared.Players.PlayTimeTracking; +using Content.Shared.Preferences; using Content.Shared.Roles; using Robust.Server.Player; using Robust.Shared.Configuration; @@ -35,6 +37,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem [Dependency] private readonly MindSystem _minds = default!; [Dependency] private readonly PlayTimeTrackingManager _tracking = default!; [Dependency] private readonly IAdminManager _adminManager = default!; + [Dependency] private readonly IServerPreferencesManager _preferencesManager = default!; public override void Initialize() { @@ -206,7 +209,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem playTimes = new Dictionary(); } - return JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes); + return JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter); } public HashSet> GetDisallowedJobs(ICommonSession player) @@ -223,7 +226,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem foreach (var job in _prototypes.EnumeratePrototypes()) { - if (JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes)) + if (JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter)) roles.Add(job.ID); } @@ -246,7 +249,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem for (var i = 0; i < jobs.Count; i++) { if (_prototypes.TryIndex(jobs[i], out var job) - && JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes)) + && JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(userId).SelectedCharacter)) { continue; } diff --git a/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs b/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs index 4a40e2c65e..2f7e7b7c48 100644 --- a/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs +++ b/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs @@ -25,8 +25,10 @@ public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect var manager = collection.Resolve(); var playtimes = manager.GetPlayTimes(session); - return JobRequirements.TryRequirementMet(Requirement, playtimes, out reason, - collection.Resolve(), - collection.Resolve()); + return Requirement.Check(collection.Resolve(), + collection.Resolve(), + profile, + playtimes, + out reason); } } diff --git a/Content.Shared/Roles/DepartmentPrototype.cs b/Content.Shared/Roles/DepartmentPrototype.cs index d6288bec90..5c94c90f8b 100644 --- a/Content.Shared/Roles/DepartmentPrototype.cs +++ b/Content.Shared/Roles/DepartmentPrototype.cs @@ -9,10 +9,16 @@ public sealed partial class DepartmentPrototype : IPrototype public string ID { get; } = string.Empty; /// - /// A description string to display in the character menu as an explanation of the department's function. + /// The name LocId of the department that will be displayed in the various menus. /// [DataField(required: true)] - public string Description = string.Empty; + public LocId Name = string.Empty; + + /// + /// A description LocId to display in the character menu as an explanation of the department's function. + /// + [DataField(required: true)] + public LocId Description = string.Empty; /// /// A color representing this department to use for text. diff --git a/Content.Shared/Roles/JobRequirement/AgeRequirement.cs b/Content.Shared/Roles/JobRequirement/AgeRequirement.cs new file mode 100644 index 0000000000..2cdc8e001c --- /dev/null +++ b/Content.Shared/Roles/JobRequirement/AgeRequirement.cs @@ -0,0 +1,50 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Preferences; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Roles; + +/// +/// Requires the character to be older or younger than a certain age (inclusive) +/// +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class AgeRequirement : JobRequirement +{ + [DataField(required: true)] + public int RequiredAge; + + public override bool Check(IEntityManager entManager, + IPrototypeManager protoManager, + HumanoidCharacterProfile? profile, + IReadOnlyDictionary playTimes, + [NotNullWhen(false)] out FormattedMessage? reason) + { + reason = new FormattedMessage(); + + if (profile is null) //the profile could be null if the player is a ghost. In this case we don't need to block the role selection for ghostrole + return true; + + if (!Inverted) + { + reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-to-young", + ("age", RequiredAge))); + + if (profile.Age <= RequiredAge) + return false; + } + else + { + reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-to-old", + ("age", RequiredAge))); + + if (profile.Age >= RequiredAge) + return false; + } + + return true; + } +} diff --git a/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs b/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs new file mode 100644 index 0000000000..56b7d8ba81 --- /dev/null +++ b/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs @@ -0,0 +1,83 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Preferences; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Roles; + +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class DepartmentTimeRequirement : JobRequirement +{ + /// + /// Which department needs the required amount of time. + /// + [DataField(required: true)] + public ProtoId Department = default!; + + /// + /// How long (in seconds) this requirement is. + /// + [DataField(required: true)] + public TimeSpan Time; + + public override bool Check(IEntityManager entManager, + IPrototypeManager protoManager, + HumanoidCharacterProfile? profile, + IReadOnlyDictionary playTimes, + [NotNullWhen(false)] out FormattedMessage? reason) + { + reason = new FormattedMessage(); + var playtime = TimeSpan.Zero; + + // Check all jobs' departments + var department = protoManager.Index(Department); + var jobs = department.Roles; + string proto; + + // Check all jobs' playtime + foreach (var other in jobs) + { + // The schema is stored on the Job role but we want to explode if the timer isn't found anyway. + proto = protoManager.Index(other).PlayTimeTracker; + + playTimes.TryGetValue(proto, out var otherTime); + playtime += otherTime; + } + + var deptDiff = Time.TotalMinutes - playtime.TotalMinutes; + var nameDepartment = "role-timer-department-unknown"; + + if (protoManager.TryIndex(Department, out var departmentIndexed)) + { + nameDepartment = departmentIndexed.Name; + } + + if (!Inverted) + { + if (deptDiff <= 0) + return true; + + reason = FormattedMessage.FromMarkupPermissive(Loc.GetString( + "role-timer-department-insufficient", + ("time", Math.Ceiling(deptDiff)), + ("department", Loc.GetString(nameDepartment)), + ("departmentColor", department.Color.ToHex()))); + return false; + } + + if (deptDiff <= 0) + { + reason = FormattedMessage.FromMarkupPermissive(Loc.GetString( + "role-timer-department-too-high", + ("time", -deptDiff), + ("department", Loc.GetString(nameDepartment)), + ("departmentColor", department.Color.ToHex()))); + return false; + } + + return true; + } +} diff --git a/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs b/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs new file mode 100644 index 0000000000..acbb8f2b4d --- /dev/null +++ b/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs @@ -0,0 +1,50 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Players.PlayTimeTracking; +using Content.Shared.Preferences; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Roles; + +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class OverallPlaytimeRequirement : JobRequirement +{ + /// + [DataField(required: true)] + public TimeSpan Time; + + public override bool Check(IEntityManager entManager, + IPrototypeManager protoManager, + HumanoidCharacterProfile? profile, + IReadOnlyDictionary playTimes, + [NotNullWhen(false)] out FormattedMessage? reason) + { + reason = new FormattedMessage(); + + var overallTime = playTimes.GetValueOrDefault(PlayTimeTrackingShared.TrackerOverall); + var overallDiff = Time.TotalMinutes - overallTime.TotalMinutes; + + if (!Inverted) + { + if (overallDiff <= 0 || overallTime >= Time) + return true; + + reason = FormattedMessage.FromMarkupPermissive(Loc.GetString( + "role-timer-overall-insufficient", + ("time", Math.Ceiling(overallDiff)))); + return false; + } + + if (overallDiff <= 0 || overallTime >= Time) + { + reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-overall-too-high", + ("time", -overallDiff))); + return false; + } + + return true; + } +} diff --git a/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs b/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs new file mode 100644 index 0000000000..658db95ab5 --- /dev/null +++ b/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs @@ -0,0 +1,73 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Players.PlayTimeTracking; +using Content.Shared.Preferences; +using Content.Shared.Roles.Jobs; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Roles; + +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class RoleTimeRequirement : JobRequirement +{ + /// + /// What particular role they need the time requirement with. + /// + [DataField(required: true)] + public ProtoId Role = default!; + + /// + [DataField(required: true)] + public TimeSpan Time; + + public override bool Check(IEntityManager entManager, + IPrototypeManager protoManager, + HumanoidCharacterProfile? profile, + IReadOnlyDictionary playTimes, + [NotNullWhen(false)] out FormattedMessage? reason) + { + reason = new FormattedMessage(); + + string proto = Role; + + playTimes.TryGetValue(proto, out var roleTime); + var roleDiff = Time.TotalMinutes - roleTime.TotalMinutes; + var departmentColor = Color.Yellow; + + if (entManager.EntitySysManager.TryGetEntitySystem(out SharedJobSystem? jobSystem)) + { + var jobProto = jobSystem.GetJobPrototype(proto); + + if (jobSystem.TryGetDepartment(jobProto, out var departmentProto)) + departmentColor = departmentProto.Color; + } + + if (!Inverted) + { + if (roleDiff <= 0) + return true; + + reason = FormattedMessage.FromMarkupPermissive(Loc.GetString( + "role-timer-role-insufficient", + ("time", Math.Ceiling(roleDiff)), + ("job", Loc.GetString(proto)), + ("departmentColor", departmentColor.ToHex()))); + return false; + } + + if (roleDiff <= 0) + { + reason = FormattedMessage.FromMarkupPermissive(Loc.GetString( + "role-timer-role-too-high", + ("time", -roleDiff), + ("job", Loc.GetString(proto)), + ("departmentColor", departmentColor.ToHex()))); + return false; + } + + return true; + } +} diff --git a/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs b/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs new file mode 100644 index 0000000000..68c069931f --- /dev/null +++ b/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs @@ -0,0 +1,59 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Preferences; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Roles; + +/// +/// Requires the character to be or not be on the list of specified species +/// +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class SpeciesRequirement : JobRequirement +{ + [DataField(required: true)] + public HashSet> Species = new(); + + public override bool Check(IEntityManager entManager, + IPrototypeManager protoManager, + HumanoidCharacterProfile? profile, + IReadOnlyDictionary playTimes, + [NotNullWhen(false)] out FormattedMessage? reason) + { + reason = new FormattedMessage(); + + if (profile is null) //the profile could be null if the player is a ghost. In this case we don't need to block the role selection for ghostrole + return true; + + var sb = new StringBuilder(); + sb.Append("[color=yellow]"); + foreach (var s in Species) + { + sb.Append(Loc.GetString(protoManager.Index(s).Name) + " "); + } + + sb.Append("[/color]"); + + if (!Inverted) + { + reason = FormattedMessage.FromMarkupPermissive($"{Loc.GetString("role-timer-whitelisted-species")}\n{sb}"); + + if (!Species.Contains(profile.Species)) + return false; + } + else + { + reason = FormattedMessage.FromMarkupPermissive($"{Loc.GetString("role-timer-blacklisted-species")}\n{sb}"); + + if (Species.Contains(profile.Species)) + return false; + } + + return true; + } +} diff --git a/Content.Shared/Roles/JobRequirements.cs b/Content.Shared/Roles/JobRequirements.cs index c9d66fcf91..17f5f7bd6a 100644 --- a/Content.Shared/Roles/JobRequirements.cs +++ b/Content.Shared/Roles/JobRequirements.cs @@ -1,228 +1,51 @@ using System.Diagnostics.CodeAnalysis; -using Content.Shared.Players.PlayTimeTracking; -using Content.Shared.Roles.Jobs; -using JetBrains.Annotations; +using Content.Shared.Preferences; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Utility; -namespace Content.Shared.Roles +namespace Content.Shared.Roles; + +public static class JobRequirements { - /// - /// Abstract class for playtime and other requirements for role gates. - /// - [ImplicitDataDefinitionForInheritors] - [Serializable, NetSerializable] - public abstract partial class JobRequirement{} - - [UsedImplicitly] - [Serializable, NetSerializable] - public sealed partial class DepartmentTimeRequirement : JobRequirement + public static bool TryRequirementsMet( + JobPrototype job, + IReadOnlyDictionary playTimes, + [NotNullWhen(false)] out FormattedMessage? reason, + IEntityManager entManager, + IPrototypeManager protoManager, + HumanoidCharacterProfile? profile) { - /// - /// Which department needs the required amount of time. - /// - [DataField("department", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Department = default!; - - /// - /// How long (in seconds) this requirement is. - /// - [DataField("time")] public TimeSpan Time; - - /// - /// If true, requirement will return false if playtime above the specified time. - /// - /// - /// False by default.
- /// True for invert general requirement - ///
- [DataField("inverted")] public bool Inverted; - } - - [UsedImplicitly] - [Serializable, NetSerializable] - public sealed partial class RoleTimeRequirement : JobRequirement - { - /// - /// What particular role they need the time requirement with. - /// - [DataField("role", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Role = default!; - - /// - [DataField("time")] public TimeSpan Time; - - /// - [DataField("inverted")] public bool Inverted; - } - - [UsedImplicitly] - [Serializable, NetSerializable] - public sealed partial class OverallPlaytimeRequirement : JobRequirement - { - /// - [DataField("time")] public TimeSpan Time; - - /// - [DataField("inverted")] public bool Inverted; - } - - public static class JobRequirements - { - public static bool TryRequirementsMet( - JobPrototype job, - IReadOnlyDictionary playTimes, - [NotNullWhen(false)] out FormattedMessage? reason, - IEntityManager entManager, - IPrototypeManager prototypes) - { - var sys = entManager.System(); - var requirements = sys.GetJobRequirement(job); - reason = null; - if (requirements == null) - return true; - - foreach (var requirement in requirements) - { - if (!TryRequirementMet(requirement, playTimes, out reason, entManager, prototypes)) - return false; - } - + var sys = entManager.System(); + var requirements = sys.GetJobRequirement(job); + reason = null; + if (requirements == null) return true; - } - /// - /// Returns a string with the reason why a particular requirement may not be met. - /// - public static bool TryRequirementMet( - JobRequirement requirement, - IReadOnlyDictionary playTimes, - [NotNullWhen(false)] out FormattedMessage? reason, - IEntityManager entManager, - IPrototypeManager prototypes) + foreach (var requirement in requirements) { - reason = null; - - switch (requirement) - { - case DepartmentTimeRequirement deptRequirement: - var playtime = TimeSpan.Zero; - - // Check all jobs' departments - var department = prototypes.Index(deptRequirement.Department); - var jobs = department.Roles; - string proto; - - // Check all jobs' playtime - foreach (var other in jobs) - { - // The schema is stored on the Job role but we want to explode if the timer isn't found anyway. - proto = prototypes.Index(other).PlayTimeTracker; - - playTimes.TryGetValue(proto, out var otherTime); - playtime += otherTime; - } - - var deptDiff = deptRequirement.Time.TotalMinutes - playtime.TotalMinutes; - - if (!deptRequirement.Inverted) - { - if (deptDiff <= 0) - return true; - - reason = FormattedMessage.FromMarkupPermissive(Loc.GetString( - "role-timer-department-insufficient", - ("time", Math.Ceiling(deptDiff)), - ("department", Loc.GetString(deptRequirement.Department)), - ("departmentColor", department.Color.ToHex()))); - return false; - } - else - { - if (deptDiff <= 0) - { - reason = FormattedMessage.FromMarkupPermissive(Loc.GetString( - "role-timer-department-too-high", - ("time", -deptDiff), - ("department", Loc.GetString(deptRequirement.Department)), - ("departmentColor", department.Color.ToHex()))); - return false; - } - - return true; - } - - case OverallPlaytimeRequirement overallRequirement: - var overallTime = playTimes.GetValueOrDefault(PlayTimeTrackingShared.TrackerOverall); - var overallDiff = overallRequirement.Time.TotalMinutes - overallTime.TotalMinutes; - - if (!overallRequirement.Inverted) - { - if (overallDiff <= 0 || overallTime >= overallRequirement.Time) - return true; - - reason = FormattedMessage.FromMarkupPermissive(Loc.GetString( - "role-timer-overall-insufficient", - ("time", Math.Ceiling(overallDiff)))); - return false; - } - else - { - if (overallDiff <= 0 || overallTime >= overallRequirement.Time) - { - reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-overall-too-high", ("time", -overallDiff))); - return false; - } - - return true; - } - - case RoleTimeRequirement roleRequirement: - proto = roleRequirement.Role; - - playTimes.TryGetValue(proto, out var roleTime); - var roleDiff = roleRequirement.Time.TotalMinutes - roleTime.TotalMinutes; - var departmentColor = Color.Yellow; - - if (entManager.EntitySysManager.TryGetEntitySystem(out SharedJobSystem? jobSystem)) - { - var jobProto = jobSystem.GetJobPrototype(proto); - - if (jobSystem.TryGetDepartment(jobProto, out var departmentProto)) - departmentColor = departmentProto.Color; - } - - if (!roleRequirement.Inverted) - { - if (roleDiff <= 0) - return true; - - reason = FormattedMessage.FromMarkupPermissive(Loc.GetString( - "role-timer-role-insufficient", - ("time", Math.Ceiling(roleDiff)), - ("job", Loc.GetString(proto)), - ("departmentColor", departmentColor.ToHex()))); - return false; - } - else - { - if (roleDiff <= 0) - { - reason = FormattedMessage.FromMarkupPermissive(Loc.GetString( - "role-timer-role-too-high", - ("time", -roleDiff), - ("job", Loc.GetString(proto)), - ("departmentColor", departmentColor.ToHex()))); - return false; - } - - return true; - } - default: - throw new NotImplementedException(); - } + if (!requirement.Check(entManager, protoManager, profile, playTimes, out reason)) + return false; } + + return true; } } + +/// +/// Abstract class for playtime and other requirements for role gates. +/// +[ImplicitDataDefinitionForInheritors] +[Serializable, NetSerializable] +public abstract partial class JobRequirement +{ + [DataField] + public bool Inverted; + + public abstract bool Check( + IEntityManager entManager, + IPrototypeManager protoManager, + HumanoidCharacterProfile? profile, + IReadOnlyDictionary playTimes, + [NotNullWhen(false)] out FormattedMessage? reason); +} diff --git a/Resources/Locale/en-US/job/role-timers.ftl b/Resources/Locale/en-US/job/role-requirements.ftl similarity index 71% rename from Resources/Locale/en-US/job/role-timers.ftl rename to Resources/Locale/en-US/job/role-requirements.ftl index 1981f5e795..906a0470af 100644 --- a/Resources/Locale/en-US/job/role-timers.ftl +++ b/Resources/Locale/en-US/job/role-requirements.ftl @@ -4,7 +4,13 @@ role-timer-overall-insufficient = You require [color=yellow]{TOSTRING($time, "0" role-timer-overall-too-high = You require [color=yellow]{TOSTRING($time, "0")}[/color] fewer minutes of playtime to play this role. (Are you trying to play a trainee role?) role-timer-role-insufficient = You require [color=yellow]{TOSTRING($time, "0")}[/color] more minutes with [color={$departmentColor}]{$job}[/color] to play this role. role-timer-role-too-high = You require[color=yellow] {TOSTRING($time, "0")}[/color] fewer minutes with [color={$departmentColor}]{$job}[/color] to play this role. (Are you trying to play a trainee role?) +role-timer-age-to-old = Your character must be under the age of [color=yellow]{$age}[/color] to play this role. +role-timer-age-to-young = Your character must be over the age of [color=yellow]{$age}[/color] to play this role. +role-timer-whitelisted-species = Your character must be one of the following species to play this role: +role-timer-blacklisted-species = Your character must not be one of the following species to play this role: role-timer-locked = Locked (hover for details) +role-timer-department-unknown = Unknown Department + role-ban = You have been banned from this role. diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml index 515e2f8425..0197cd0daf 100644 --- a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml +++ b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml @@ -15,6 +15,8 @@ time: 36000 #10 hours - !type:OverallPlaytimeRequirement time: 144000 #40 hrs + - !type:AgeRequirement + requiredAge: 20 weight: 10 startingGear: QuartermasterGear icon: "JobIconQuarterMaster" diff --git a/Resources/Prototypes/Roles/Jobs/Command/captain.yml b/Resources/Prototypes/Roles/Jobs/Command/captain.yml index 79634aa5d9..e2687dd653 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/captain.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/captain.yml @@ -16,6 +16,8 @@ - !type:DepartmentTimeRequirement department: Command time: 54000 # 15 hours + - !type:AgeRequirement + requiredAge: 20 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 d5521f767f..c1b285303d 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml @@ -16,6 +16,8 @@ - !type:DepartmentTimeRequirement department: Command time: 36000 # 10 hours + - !type:AgeRequirement + requiredAge: 20 weight: 20 startingGear: HoPGear icon: "JobIconHeadOfPersonnel" diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml index 0ee0b6736c..c0d9569c5d 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml @@ -15,6 +15,8 @@ time: 36000 #10 hrs - !type:OverallPlaytimeRequirement time: 144000 #40 hrs + - !type:AgeRequirement + requiredAge: 20 weight: 10 startingGear: ChiefEngineerGear icon: "JobIconChiefEngineer" diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml index 2587113415..94248450cd 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml @@ -17,6 +17,8 @@ time: 36000 #10 hrs - !type:OverallPlaytimeRequirement time: 144000 #40 hrs + - !type:AgeRequirement + requiredAge: 20 weight: 10 startingGear: CMOGear icon: "JobIconChiefMedicalOfficer" diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml index b54ba54b1a..b66e89f420 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml @@ -9,6 +9,8 @@ time: 36000 #10 hrs - !type:OverallPlaytimeRequirement time: 144000 #40 hrs + - !type:AgeRequirement + requiredAge: 20 weight: 10 startingGear: ResearchDirectorGear icon: "JobIconResearchDirector" diff --git a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml index 044df7f69e..c02f1cee76 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml @@ -15,6 +15,8 @@ time: 108000 # 30 hrs - !type:OverallPlaytimeRequirement time: 144000 #40 hrs + - !type:AgeRequirement + requiredAge: 20 weight: 10 startingGear: HoSGear icon: "JobIconHeadOfSecurity" diff --git a/Resources/Prototypes/Roles/Jobs/departments.yml b/Resources/Prototypes/Roles/Jobs/departments.yml index 9be98be950..6d25d8fd88 100644 --- a/Resources/Prototypes/Roles/Jobs/departments.yml +++ b/Resources/Prototypes/Roles/Jobs/departments.yml @@ -1,5 +1,6 @@ - type: department id: Cargo + name: department-Cargo description: department-Cargo-description color: "#A46106" roles: @@ -9,6 +10,7 @@ - type: department id: Civilian + name: department-Civilian description: department-Civilian-description color: "#9FED58" weight: -10 @@ -34,6 +36,7 @@ - type: department id: Command + name: department-Command description: department-Command-description color: "#334E6D" roles: @@ -50,6 +53,7 @@ - type: department id: Engineering + name: department-Engineering description: department-Engineering-description color: "#EFB341" roles: @@ -60,6 +64,7 @@ - type: department id: Medical + name: department-Medical description: department-Medical-description color: "#52B4E9" roles: @@ -72,6 +77,7 @@ - type: department id: Security + name: department-Security description: department-Security-description color: "#DE3A3A" weight: 20 @@ -84,6 +90,7 @@ - type: department id: Science + name: department-Science description: department-Science-description color: "#D381C9" roles: @@ -93,6 +100,7 @@ - type: department id: Specific + name: department-Specific description: department-Specific-description color: "#9FED58" weight: 10