diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
index df537509fe..952c9ff91e 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
@@ -565,6 +565,7 @@ namespace Content.Client.Lobby.UI
}
Traits.SetSelectedTraits(selectedTraits);
+ Traits.UpdateConditions(Profile.Species);
}
// End DeltaV - Traits Integration
diff --git a/Content.Client/_DV/Traits/DisabledTraitsPopupSystem.cs b/Content.Client/_DV/Traits/DisabledTraitsPopupSystem.cs
new file mode 100644
index 0000000000..d62f7ed7be
--- /dev/null
+++ b/Content.Client/_DV/Traits/DisabledTraitsPopupSystem.cs
@@ -0,0 +1,51 @@
+using Content.Client._DV.Traits.UI;
+using Content.Shared._DV.CCVars;
+using Content.Shared._DV.Traits;
+using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._DV.Traits;
+
+///
+/// Client system that shows a popup when traits are disabled due to unmet conditions.
+///
+public sealed class DisabledTraitsPopupSystem : EntitySystem
+{
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
+ private DisabledTraitsPopup? _window;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeNetworkEvent(OnDisabledTraits);
+ }
+
+ private void OnDisabledTraits(DisabledTraitsEvent ev)
+ {
+ // Don't show if user has opted to skip this popup
+ if (_cfg.GetCVar(DCCVars.SkipDisabledTraitsPopup))
+ return;
+
+ // Don't show if no traits were actually disabled
+ if (ev.DisabledTraits.Count == 0)
+ return;
+
+ OpenDisabledTraitsPopup(ev.DisabledTraits);
+ }
+
+ private void OpenDisabledTraitsPopup(Dictionary, List> disabledTraits)
+ {
+ // Close existing window if one is open
+ if (_window != null)
+ {
+ _window.Close();
+ _window = null;
+ }
+
+ _window = new DisabledTraitsPopup(disabledTraits);
+ _window.OpenCentered();
+ _window.OnClose += () => _window = null;
+ }
+}
diff --git a/Content.Client/_DV/Traits/UI/DisabledTraitsPopup.xaml b/Content.Client/_DV/Traits/UI/DisabledTraitsPopup.xaml
new file mode 100644
index 0000000000..59d6b83412
--- /dev/null
+++ b/Content.Client/_DV/Traits/UI/DisabledTraitsPopup.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_DV/Traits/UI/DisabledTraitsPopup.xaml.cs b/Content.Client/_DV/Traits/UI/DisabledTraitsPopup.xaml.cs
new file mode 100644
index 0000000000..7045b73601
--- /dev/null
+++ b/Content.Client/_DV/Traits/UI/DisabledTraitsPopup.xaml.cs
@@ -0,0 +1,101 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._DV.CCVars;
+using Content.Shared._DV.Traits;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client._DV.Traits.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class DisabledTraitsPopup : FancyWindow
+{
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ private bool _initialSkipState;
+
+ public DisabledTraitsPopup(Dictionary, List> disabledTraits)
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+
+ InitializeUI(disabledTraits);
+ InitializeEvents();
+ }
+
+ private void InitializeUI(Dictionary, List> disabledTraits)
+ {
+ TitleLabel.Text = Loc.GetString("disabled-traits-popup-label");
+ MessageLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(
+ Loc.GetString("disabled-traits-popup-message")));
+
+ _initialSkipState = _cfg.GetCVar(DCCVars.SkipDisabledTraitsPopup);
+ SkipCheckBox.Pressed = _initialSkipState;
+
+ PopulateDisabledTraits(disabledTraits);
+ }
+
+ private void PopulateDisabledTraits(Dictionary, List> disabledTraits)
+ {
+ DisabledTraitsContainer.RemoveAllChildren();
+
+ foreach (var (traitId, reasons) in disabledTraits)
+ {
+ if (!_prototype.TryIndex(traitId, out var trait))
+ continue;
+
+ var container = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ Margin = new Thickness(0, 0, 0, 10)
+ };
+
+ // Trait name
+ var nameLabel = new Label
+ {
+ Text = Loc.GetString(trait.Name),
+ FontColorOverride = Color.FromHex("#ff6b6b"),
+ StyleClasses = { "LabelSubText" }
+ };
+ container.AddChild(nameLabel);
+
+ // Reasons why it was disabled
+ foreach (var reason in reasons)
+ {
+ var reasonLabel = new RichTextLabel
+ {
+ Margin = new Thickness(10, 2, 0, 0)
+ };
+ reasonLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(
+ Loc.GetString("disabled-traits-popup-reason", ("reason", reason))));
+ container.AddChild(reasonLabel);
+ }
+
+ DisabledTraitsContainer.AddChild(container);
+ }
+ }
+
+ private void InitializeEvents()
+ {
+ OnClose += SaveSkipState;
+ ButtonClose.OnPressed += OnClosePressed;
+ }
+
+ private void SaveSkipState()
+ {
+ if (SkipCheckBox.Pressed == _initialSkipState)
+ return;
+
+ _cfg.SetCVar(DCCVars.SkipDisabledTraitsPopup, SkipCheckBox.Pressed);
+ _cfg.SaveToFile();
+ }
+
+ private void OnClosePressed(BaseButton.ButtonEventArgs args)
+ {
+ Close();
+ }
+}
diff --git a/Content.Client/_DV/Traits/UI/TraitCategory.xaml.cs b/Content.Client/_DV/Traits/UI/TraitCategory.xaml.cs
index 3aad0a5086..69654424e3 100644
--- a/Content.Client/_DV/Traits/UI/TraitCategory.xaml.cs
+++ b/Content.Client/_DV/Traits/UI/TraitCategory.xaml.cs
@@ -1,5 +1,7 @@
using System.Linq;
using Content.Shared._DV.Traits;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -169,7 +171,7 @@ public sealed partial class TraitCategory : BoxContainer
/// Updates condition states for all trait entries based on current job/species.
/// Traits that don't meet conditions are disabled but still visible.
///
- public void UpdateConditions(string? jobId, string? speciesId)
+ public void UpdateConditions(ProtoId? jobId, ProtoId? speciesId)
{
foreach (var (_, entry) in _traitEntries)
{
diff --git a/Content.Client/_DV/Traits/UI/TraitEntry.xaml b/Content.Client/_DV/Traits/UI/TraitEntry.xaml
index 821e195daf..3f00f9c35c 100644
--- a/Content.Client/_DV/Traits/UI/TraitEntry.xaml
+++ b/Content.Client/_DV/Traits/UI/TraitEntry.xaml
@@ -7,11 +7,27 @@
HorizontalExpand="True"
Margin="10 8">
-
-
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_DV/Traits/UI/TraitEntry.xaml.cs b/Content.Client/_DV/Traits/UI/TraitEntry.xaml.cs
index d9ec9a7076..9aafc5fe71 100644
--- a/Content.Client/_DV/Traits/UI/TraitEntry.xaml.cs
+++ b/Content.Client/_DV/Traits/UI/TraitEntry.xaml.cs
@@ -1,5 +1,7 @@
using Content.Shared._DV.Traits;
using Content.Shared._DV.Traits.Conditions;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
@@ -18,7 +20,8 @@ public sealed partial class TraitEntry : PanelContainer
public event Action? OnToggled;
public bool IsSelected => TraitCheckbox.Pressed;
- public int TraitCost { get; }
+ public readonly int TraitCost;
+ public bool MeetsConditions { get; private set; } = true;
private readonly TraitPrototype _trait;
private bool _isUpdating;
@@ -88,7 +91,7 @@ public sealed partial class TraitEntry : PanelContainer
///
/// Updates whether conditions are met based on current job/species.
///
- public void UpdateConditionsMet(string? jobId, string? speciesId)
+ public void UpdateConditionsMet(ProtoId? jobId, ProtoId? speciesId)
{
_failedConditionTooltips.Clear();
MeetsConditions = true;
@@ -100,6 +103,8 @@ public sealed partial class TraitEntry : PanelContainer
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
+ HasCompCondition compCond => !compCond.Invert, // can't check in lobby but screws with the inversion logic
+ AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId),
_ => true,
};
@@ -118,39 +123,71 @@ public sealed partial class TraitEntry : PanelContainer
UpdateDisabledState();
}
- private bool CheckSpeciesCondition(IsSpeciesCondition condition, string? speciesId)
+ private bool CheckSpeciesCondition(IsSpeciesCondition condition, ProtoId? speciesId)
{
- if (string.IsNullOrEmpty(speciesId))
+ if (!_prototype.TryIndex(speciesId, out var species))
return false;
- return speciesId == condition.Species.Id;
+ return species == condition.Species;
}
- private bool CheckJobCondition(HasJobCondition condition, string? jobId)
+ private bool CheckJobCondition(HasJobCondition condition, ProtoId? jobId)
{
- if (string.IsNullOrEmpty(jobId))
+ if (!_prototype.TryIndex(jobId, out var job))
return false;
- return jobId == condition.Job;
+ return job == condition.Job;
}
- private bool CheckDepartmentCondition(InDepartmentCondition condition, string? jobId)
+ private bool CheckDepartmentCondition(InDepartmentCondition condition, ProtoId? jobId)
{
- if (string.IsNullOrEmpty(jobId))
+ if (!_prototype.TryIndex(jobId, out var job))
return false;
if (!_prototype.TryIndex(condition.Department, out var department))
return false;
- return department.Roles.Contains(jobId);
+ return department.Roles.Contains(job);
+ }
+
+ private bool CheckAnyOfCondition(AnyOfCondition condition, ProtoId? jobId, ProtoId? speciesId)
+ {
+ if (condition.Conditions.Count == 0)
+ return false;
+
+ // Return true if ANY child condition evaluates to true
+ foreach (var childCondition in condition.Conditions)
+ {
+ var result = childCondition switch
+ {
+ IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
+ HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
+ InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
+ HasCompCondition compCond => !compCond.Invert, // can't check in lobby
+ AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId), // Recursive!
+ _ => true,
+ };
+
+ // Apply child's inversion
+ result ^= childCondition.Invert;
+
+ // If any child passes, the AnyOf passes
+ if (result)
+ return true;
+ }
+
+ // None of the children passed
+ return false;
}
private void UpdateDisabledState()
{
- TraitCheckbox.Disabled = !MeetsConditions;
-
if (!MeetsConditions)
{
+ // Hide checkbox, show lock icon
+ TraitCheckbox.Visible = false;
+ LockIcon.Visible = true;
+
// Deselect if conditions no longer met
if (TraitCheckbox.Pressed)
{
@@ -161,7 +198,7 @@ public sealed partial class TraitEntry : PanelContainer
OnToggled?.Invoke(false);
}
- // Show why it's disabled
+ // Add disabled styling
AddStyleClass("TraitsEntryDisabled");
// Update tooltip to show failed conditions
@@ -175,8 +212,15 @@ public sealed partial class TraitEntry : PanelContainer
}
else
{
+ // Show checkbox, hide lock icon
+ TraitCheckbox.Visible = true;
+ LockIcon.Visible = false;
+
+ // Remove disabled styling - stylesheet restores normal colors
RemoveStyleClass("TraitsEntryDisabled");
- UpdateConditionTooltips(); // Reset to normal tooltips
+
+ // Reset to normal tooltips
+ UpdateConditionTooltips();
}
}
@@ -187,7 +231,7 @@ public sealed partial class TraitEntry : PanelContainer
if (!MeetsConditions)
{
- // Prevent selection if conditions not met
+ // This shouldn't happen since checkbox is hidden, but just in case
_isUpdating = true;
TraitCheckbox.Pressed = false;
_isUpdating = false;
@@ -206,8 +250,6 @@ public sealed partial class TraitEntry : PanelContainer
_isUpdating = false;
}
- public bool MeetsConditions { get; private set; } = true;
-
private void UpdateSelectedStyle()
{
if (TraitCheckbox.Pressed)
diff --git a/Content.Client/_DV/Traits/UI/TraitsSheetlet.cs b/Content.Client/_DV/Traits/UI/TraitsSheetlet.cs
index 63c98dc814..7de88e6423 100644
--- a/Content.Client/_DV/Traits/UI/TraitsSheetlet.cs
+++ b/Content.Client/_DV/Traits/UI/TraitsSheetlet.cs
@@ -77,6 +77,14 @@ public sealed class TraitsSheetlet : Sheetlet where T : PalettedStylesheet
};
entrySelectedBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
+ var entryDisabledBox = new StyleBoxFlat
+ {
+ BackgroundColor = Color.FromHex("#1a1a22"),
+ BorderColor = Color.FromHex("#2a2a2a"),
+ BorderThickness = new Thickness(1)
+ };
+ entryDisabledBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
+
var progressBarBgBox = new StyleBoxFlat
{
BackgroundColor = bgDark,
@@ -162,7 +170,7 @@ public sealed class TraitsSheetlet : Sheetlet where T : PalettedStylesheet
E