}
var player = _playerManager.LocalPlayer?.Session;
-
if (player == null)
return true;
- var reasonBuilder = new StringBuilder();
+ return CheckRoleTime(job.Requirements, out reason);
+ }
+
+ public bool CheckRoleTime(HashSet<JobRequirement>? requirements, [NotNullWhen(false)] out FormattedMessage? reason)
+ {
+ reason = null;
+
+ if (requirements == null)
+ return true;
- foreach (var requirement in job.Requirements)
+ var reasons = new List<string>();
+ foreach (var requirement in requirements)
{
if (JobRequirements.TryRequirementMet(requirement, _roles, out var jobReason, _entManager, _prototypes))
continue;
- reasonBuilder.AppendLine(jobReason.ToMarkup());
+ reasons.Add(jobReason.ToMarkup());
}
- reason = reasonBuilder.Length == 0 ? null : FormattedMessage.FromMarkup(reasonBuilder.ToString().Trim());
+ reason = reasons.Count == 0 ? null : FormattedMessage.FromMarkup(string.Join('\n', reasons));
return reason == null;
}
}
foreach (var antag in prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
{
if (!antag.SetPreference)
- {
continue;
- }
var selector = new AntagPreferenceSelector(antag);
_antagList.AddChild(selector);
_antagPreferences.Add(selector);
+ if (selector.Disabled)
+ {
+ Profile = Profile?.WithAntagPreference(antag.ID, false);
+ IsDirty = true;
+ }
selector.PreferenceChanged += preference =>
{
foreach (var jobSelector in _jobPriorities)
{
// Sync other selectors with the same job in case of multiple department jobs
- if (jobSelector.Job == selector.Job)
+ if (jobSelector.Proto == selector.Proto)
{
jobSelector.Priority = priority;
}
-
- // Lower any other high priorities to medium.
- if (priority == JobPriority.High)
+ else if (priority == JobPriority.High && jobSelector.Priority == JobPriority.High)
{
- if (jobSelector.Job != selector.Job && jobSelector.Priority == JobPriority.High)
- {
- jobSelector.Priority = JobPriority.Medium;
- Profile = Profile?.WithJobPriority(jobSelector.Job.ID, JobPriority.Medium);
- }
+ // Lower any other high priorities to medium.
+ jobSelector.Priority = JobPriority.Medium;
+ Profile = Profile?.WithJobPriority(jobSelector.Proto.ID, JobPriority.Medium);
}
}
};
{
foreach (var prioritySelector in _jobPriorities)
{
- var jobId = prioritySelector.Job.ID;
+ var jobId = prioritySelector.Proto.ID;
var priority = Profile?.JobPriorities.GetValueOrDefault(jobId, JobPriority.Never) ?? JobPriority.Never;
}
}
- private sealed class JobPrioritySelector : Control
+ private abstract class RequirementsSelector<T> : Control
{
- public JobPrototype Job { get; }
- private readonly RadioOptions<int> _optionButton;
-
- public JobPriority Priority
- {
- get => (JobPriority) _optionButton.SelectedValue;
- set => _optionButton.SelectByValue((int) value);
- }
-
- public event Action<JobPriority>? PriorityChanged;
+ public T Proto { get; }
+ public bool Disabled => _lockStripe.Visible;
+ protected readonly RadioOptions<int> Options;
private StripeBack _lockStripe;
private Label _requirementsLabel;
- private Label _jobTitle;
- public JobPrioritySelector(JobPrototype job, IPrototypeManager prototypeManager)
+ protected RequirementsSelector(T proto)
{
- Job = job;
+ Proto = proto;
- _optionButton = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
+ Options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
{
FirstButtonStyle = StyleBase.ButtonOpenRight,
ButtonStyle = StyleBase.ButtonOpenBoth,
LastButtonStyle = StyleBase.ButtonOpenLeft
};
//Override default radio option button width
- _optionButton.GenerateItem = GenerateButton;
- // Text, Value
- _optionButton.AddItem(Loc.GetString("humanoid-profile-editor-job-priority-high-button"), (int) JobPriority.High);
- _optionButton.AddItem(Loc.GetString("humanoid-profile-editor-job-priority-medium-button"), (int) JobPriority.Medium);
- _optionButton.AddItem(Loc.GetString("humanoid-profile-editor-job-priority-low-button"), (int) JobPriority.Low);
- _optionButton.AddItem(Loc.GetString("humanoid-profile-editor-job-priority-never-button"), (int) JobPriority.Never);
-
- _optionButton.OnItemSelected += args =>
- {
- _optionButton.Select(args.Id);
- PriorityChanged?.Invoke(Priority);
- };
-
- var icon = new TextureRect
- {
- TextureScale = new Vector2(2, 2),
- Stretch = TextureRect.StretchMode.KeepCentered
- };
+ Options.GenerateItem = GenerateButton;
- var jobIcon = prototypeManager.Index<StatusIconPrototype>(job.Icon);
- icon.Texture = jobIcon.Icon.Frame0();
+ Options.OnItemSelected += args => Options.Select(args.Id);
_requirementsLabel = new Label()
{
}
};
- _jobTitle = new Label()
- {
- Margin = new Thickness(5f,0,5f,0),
- Text = job.LocalizedName,
- MinSize = new Vector2(200, 0),
- MouseFilter = MouseFilterMode.Stop
- };
+ // Setup must be called after
+ }
- if (job.LocalizedDescription != null)
+ /// <summary>
+ /// Actually adds the controls, must be called in the inheriting class' constructor.
+ /// </summary>
+ protected void Setup((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
+ {
+ foreach (var (text, value) in items)
{
- _jobTitle.ToolTip = job.LocalizedDescription;
+ Options.AddItem(Loc.GetString(text), value);
}
- AddChild(new BoxContainer
+ var titleLabel = new Label()
+ {
+ Margin = new Thickness(5f, 0, 5f, 0),
+ Text = title,
+ MinSize = new Vector2(titleSize, 0),
+ MouseFilter = MouseFilterMode.Stop,
+ ToolTip = description
+ };
+
+ var container = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
- Children =
- {
- icon,
- _jobTitle,
- _optionButton,
- _lockStripe,
- }
- });
+ };
+
+ if (icon != null)
+ container.AddChild(icon);
+ container.AddChild(titleLabel);
+ container.AddChild(Options);
+ container.AddChild(_lockStripe);
+
+ AddChild(container);
}
public void LockRequirements(FormattedMessage requirements)
tooltip.SetMessage(requirements);
_lockStripe.TooltipSupplier = _ => tooltip;
_lockStripe.Visible = true;
- _optionButton.Visible = false;
+ Options.Visible = false;
}
// TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
public void UnlockRequirements()
{
- _requirementsLabel.Visible = false;
_lockStripe.Visible = false;
- _optionButton.Visible = true;
+ Options.Visible = true;
}
private Button GenerateButton(string text, int value)
{
- var btn = new Button
+ return new Button
{
Text = text,
MinWidth = 90
};
- return btn;
+ }
+ }
+
+ private sealed class JobPrioritySelector : RequirementsSelector<JobPrototype>
+ {
+ public JobPriority Priority
+ {
+ get => (JobPriority) Options.SelectedValue;
+ set => Options.SelectByValue((int) value);
+ }
+
+ public event Action<JobPriority>? PriorityChanged;
+
+ public JobPrioritySelector(JobPrototype proto, IPrototypeManager protoMan)
+ : base(proto)
+ {
+ Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
+
+ var items = new[]
+ {
+ ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
+ ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
+ ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
+ ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
+ };
+
+ var icon = new TextureRect
+ {
+ TextureScale = new Vector2(2, 2),
+ Stretch = TextureRect.StretchMode.KeepCentered
+ };
+ var jobIcon = protoMan.Index<StatusIconPrototype>(proto.Icon);
+ icon.Texture = jobIcon.Icon.Frame0();
+
+ Setup(items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
}
}
{
foreach (var preferenceSelector in _antagPreferences)
{
- var antagId = preferenceSelector.Antag.ID;
+ var antagId = preferenceSelector.Proto.ID;
var preference = Profile?.AntagPreferences.Contains(antagId) ?? false;
-
preferenceSelector.Preference = preference;
}
}
}
}
- private sealed class AntagPreferenceSelector : Control
+ private sealed class AntagPreferenceSelector : RequirementsSelector<AntagPrototype>
{
- public AntagPrototype Antag { get; }
- private readonly CheckBox _checkBox;
-
+ // 0 is yes and 1 is no
public bool Preference
{
- get => _checkBox.Pressed;
- set => _checkBox.Pressed = value;
+ get => Options.SelectedValue == 0;
+ set => Options.Select((value && !Disabled) ? 0 : 1);
}
public event Action<bool>? PreferenceChanged;
- public AntagPreferenceSelector(AntagPrototype antag)
+ public AntagPreferenceSelector(AntagPrototype proto)
+ : base(proto)
{
- Antag = antag;
-
- _checkBox = new CheckBox {Text = Loc.GetString(antag.Name)};
- _checkBox.OnToggled += OnCheckBoxToggled;
+ Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
- if (antag.Description != null)
+ var items = new[]
{
- _checkBox.ToolTip = Loc.GetString(antag.Description);
- }
-
- AddChild(new BoxContainer
+ ("humanoid-profile-editor-antag-preference-yes-button", 0),
+ ("humanoid-profile-editor-antag-preference-no-button", 1)
+ };
+ var title = Loc.GetString(proto.Name);
+ var description = Loc.GetString(proto.Objective);
+ Setup(items, title, 250, description);
+
+ // immediately lock requirements if they arent met.
+ // another function checks Disabled after creating the selector so this has to be done now
+ var requirements = IoCManager.Resolve<JobRequirementsManager>();
+ if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
{
- Orientation = LayoutOrientation.Horizontal,
- Children =
- {
- _checkBox
- }
- });
- }
-
- private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
- {
- PreferenceChanged?.Invoke(Preference);
+ LockRequirements(reason);
+ }
}
}
Access="Public"
Text="{Loc 'ghost-roles-window-request-role-button'}"
StyleClasses="OpenRight"
- HorizontalAlignment="Left"/>
+ HorizontalAlignment="Left"
+ SetWidth="150"/>
<Button Name="FollowButton"
Access="Public"
Text="{Loc 'ghost-roles-window-follow-role-button'}"
StyleClasses="OpenLeft"
- HorizontalAlignment="Right"/>
+ HorizontalAlignment="Right"
+ SetWidth="150"/>
</BoxContainer>
<DefaultWindow xmlns="https://spacestation14.io"
- Title="{Loc 'ghost-roles-window-title'}">
+ Title="{Loc 'ghost-roles-window-title'}"
+ MinSize="500 300"
+ SetSize="500 300">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True">
<RichTextLabel Name="TopBanner" VerticalExpand="True"/>
+using System.Numerics;
using Content.Shared.Ghost.Roles;
using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
{
[GenerateTypedNameReferences]
public sealed partial class GhostRolesEntry : BoxContainer
{
+ private SpriteSystem _spriteSystem;
public event Action<GhostRoleInfo>? OnRoleSelected;
public event Action<GhostRoleInfo>? OnRoleFollow;
- public GhostRolesEntry(string name, string description, IEnumerable<GhostRoleInfo> roles)
+ public GhostRolesEntry(string name, string description, bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
{
RobustXamlLoader.Load(this);
+ _spriteSystem = spriteSystem;
Title.Text = name;
Description.SetMessage(description);
button.RequestButton.OnPressed += _ => OnRoleSelected?.Invoke(role);
button.FollowButton.OnPressed += _ => OnRoleFollow?.Invoke(role);
+ if (!hasAccess)
+ {
+ button.RequestButton.Disabled = true;
+
+ if (reason != null && !reason.IsEmpty)
+ {
+ var tooltip = new Tooltip();
+ tooltip.SetMessage(reason);
+ button.RequestButton.TooltipSupplier = _ => tooltip;
+ }
+
+ button.RequestButton.AddChild(new TextureRect
+ {
+ TextureScale = new Vector2(0.4f, 0.4f),
+ Stretch = TextureRect.StretchMode.KeepCentered,
+ Texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ("/Textures/Interface/Nano/lock.svg.192dpi.png"))),
+ HorizontalExpand = true,
+ HorizontalAlignment = HAlignment.Right,
+ });
+ }
+
Buttons.AddChild(button);
}
}
using System.Linq;
using Content.Client.Eui;
+using Content.Client.Players.PlayTimeTracking;
using Content.Shared.Eui;
using Content.Shared.Ghost.Roles;
using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
{
if (state is not GhostRolesEuiState ghostState) return;
_window.ClearEntries();
+ var entityManager = IoCManager.Resolve<IEntityManager>();
+ var sysManager = entityManager.EntitySysManager;
+ var spriteSystem = sysManager.GetEntitySystem<SpriteSystem>();
+ var requirementsManager = IoCManager.Resolve<JobRequirementsManager>();
+
var groupedRoles = ghostState.GhostRoles.GroupBy(
- role => (role.Name, role.Description));
+ role => (role.Name, role.Description, role.Requirements));
foreach (var group in groupedRoles)
{
var name = group.Key.Name;
var description = group.Key.Description;
+ bool hasAccess = true;
+ FormattedMessage? reason;
+
+ if (!requirementsManager.CheckRoleTime(group.Key.Requirements, out reason))
+ {
+ hasAccess = false;
+ }
- _window.AddEntry(name, description, group);
+ _window.AddEntry(name, description, hasAccess, reason, group, spriteSystem);
}
var closeRulesWindow = ghostState.GhostRoles.All(role => role.Identifier != _windowRulesId);
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'ghost-roles-window-title'}"
- MinSize="375 275">
+ MinSize="450 400"
+ SetSize="400 500">
<Label Name="NoRolesMessage"
Text="{Loc 'ghost-roles-window-no-roles-available-label'}"
VerticalAlignment="Top" />
using Content.Shared.Ghost.Roles;
using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
{
EntryContainer.DisposeAllChildren();
}
- public void AddEntry(string name, string description, IEnumerable<GhostRoleInfo> roles)
+ public void AddEntry(string name, string description, bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
{
NoRolesMessage.Visible = false;
- var entry = new GhostRolesEntry(name, description, roles);
+ var entry = new GhostRolesEntry(name, description, hasAccess, reason, roles, spriteSystem);
entry.OnRoleSelected += OnRoleRequested;
entry.OnRoleFollow += OnRoleFollow;
EntryContainer.AddChild(entry);
using Content.Server.Mind.Commands;
+using Content.Shared.Roles;
namespace Content.Server.Ghost.Roles.Components
{
[DataField("rules")] private string _roleRules = "";
+ [DataField("requirements")]
+ public HashSet<JobRequirement>? Requirements;
+
/// <summary>
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
/// </summary>
if (metaQuery.GetComponent(uid).EntityPaused)
continue;
- roles.Add(new GhostRoleInfo {Identifier = id, Name = role.RoleName, Description = role.RoleDescription, Rules = role.RoleRules});
+ roles.Add(new GhostRoleInfo {Identifier = id, Name = role.RoleName, Description = role.RoleDescription, Rules = role.RoleRules, Requirements = role.Requirements});
}
return roles.ToArray();
using Content.Shared.Eui;
+using Content.Shared.Roles;
using Robust.Shared.Serialization;
namespace Content.Shared.Ghost.Roles
public string Name { get; set; }
public string Description { get; set; }
public string Rules { get; set; }
+ public HashSet<JobRequirement>? Requirements { get; set; }
}
[NetSerializable, Serializable]
using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
-namespace Content.Shared.Roles
+namespace Content.Shared.Roles;
+
+/// <summary>
+/// Describes information for a single antag.
+/// </summary>
+[Prototype("antag")]
+[Serializable, NetSerializable]
+public sealed class AntagPrototype : IPrototype
{
+ [ViewVariables]
+ [IdDataField]
+ public string ID { get; private set; } = default!;
+
/// <summary>
- /// Describes information for a single antag.
+ /// The name of this antag as displayed to players.
/// </summary>
- [Prototype("antag")]
- public sealed class AntagPrototype : IPrototype
- {
- [ViewVariables]
- [IdDataField]
- public string ID { get; private set; } = default!;
-
- /// <summary>
- /// The name of this antag as displayed to players.
- /// </summary>
- [DataField("name")]
- public string Name { get; private set; } = "";
+ [DataField("name")]
+ public string Name { get; private set; } = "";
- /// <summary>
- /// The description of this antag shown in a tooltip.
- /// </summary>
- [DataField("description")]
- public string? Description { get; private set; }
+ /// <summary>
+ /// The antag's objective, shown in a tooltip in the antag preference menu or as a ghost role description.
+ /// </summary>
+ [DataField("objective", required: true)]
+ public string Objective { get; private set; } = "";
- /// <summary>
- /// The antag's objective, displayed at round-start to the player.
- /// </summary>
- [DataField("objective")]
- public string Objective { get; private set; } = "";
+ /// <summary>
+ /// Whether or not the antag role is one of the bad guys.
+ /// </summary>
+ [DataField("antagonist")]
+ public bool Antagonist { get; private set; }
- /// <summary>
- /// Whether or not the antag role is one of the bad guys.
- /// </summary>
- [DataField("antagonist")]
- public bool Antagonist { get; private set; }
+ /// <summary>
+ /// Whether or not the player can set the antag role in antag preferences.
+ /// </summary>
+ [DataField("setPreference")]
+ public bool SetPreference { get; private set; }
- /// <summary>
- /// Whether or not the player can set the antag role in antag preferences.
- /// </summary>
- [DataField("setPreference")]
- public bool SetPreference { get; private set; }
- }
+ /// <summary>
+ /// Requirements that must be met to opt in to this antag role.
+ /// </summary>
+ [DataField("requirements")]
+ public HashSet<JobRequirement>? Requirements;
}
using Content.Shared.Roles.Jobs;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
/// Abstract class for playtime and other requirements for role gates.
/// </summary>
[ImplicitDataDefinitionForInheritors]
+ [Serializable, NetSerializable]
public abstract partial class JobRequirement{}
[UsedImplicitly]
+ [Serializable, NetSerializable]
public sealed partial class DepartmentTimeRequirement : JobRequirement
{
/// <summary>
}
[UsedImplicitly]
+ [Serializable, NetSerializable]
public sealed partial class RoleTimeRequirement : JobRequirement
{
/// <summary>
}
[UsedImplicitly]
+ [Serializable, NetSerializable]
public sealed partial class OverallPlaytimeRequirement : JobRequirement
{
/// <inheritdoc cref="DepartmentTimeRequirement.Time"/>
return false;
}
-
return true;
}
default:
humanoid-profile-editor-jobs-amount-in-department-tooltip = Jobs in the {$departmentName} department
humanoid-profile-editor-department-jobs-label = {$departmentName} jobs
humanoid-profile-editor-antags-tab = Antags
+humanoid-profile-editor-antag-preference-yes-button = Yes
+humanoid-profile-editor-antag-preference-no-button = No
humanoid-profile-editor-traits-tab = Traits
humanoid-profile-editor-job-priority-high-button = High
humanoid-profile-editor-job-priority-medium-button = Medium