more traits improvements (#5285)
* wowie * whoopsie * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * WHY DID I DO THAT --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
df364a806a
commit
884cf115c6
|
|
@ -565,6 +565,7 @@ namespace Content.Client.Lobby.UI
|
|||
}
|
||||
|
||||
Traits.SetSelectedTraits(selectedTraits);
|
||||
Traits.UpdateConditions(Profile.Species);
|
||||
}
|
||||
// End DeltaV - Traits Integration
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Client system that shows a popup when traits are disabled due to unmet conditions.
|
||||
/// </summary>
|
||||
public sealed class DisabledTraitsPopupSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private DisabledTraitsPopup? _window;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<DisabledTraitsEvent>(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<ProtoId<TraitPrototype>, List<string>> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc disabled-traits-popup-title}"
|
||||
MinSize="400 200"
|
||||
SetSize="500 350">
|
||||
<BoxContainer Orientation="Vertical" Margin="10">
|
||||
<!-- Header -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 10">
|
||||
<TextureRect TexturePath="/Textures/Interface/info.svg.192dpi.png"
|
||||
TextureScale="0.5 0.5"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 10 0" />
|
||||
<Label Name="TitleLabel"
|
||||
StyleClasses="LabelHeading"
|
||||
VAlign="Center" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Message -->
|
||||
<RichTextLabel Name="MessageLabel"
|
||||
Margin="0 0 0 15" />
|
||||
|
||||
<!-- List of disabled traits -->
|
||||
<Label Text="{Loc disabled-traits-popup-list-header}"
|
||||
StyleClasses="LabelSubText"
|
||||
Margin="0 0 0 5" />
|
||||
|
||||
<PanelContainer StyleClasses="BackgroundDark" VerticalExpand="True">
|
||||
<ScrollContainer HScrollEnabled="False" VerticalExpand="True">
|
||||
<BoxContainer Name="DisabledTraitsContainer"
|
||||
Orientation="Vertical"
|
||||
Margin="5" />
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Footer with checkbox and close button -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
|
||||
<CheckBox Name="SkipCheckBox"
|
||||
Text="{Loc disabled-traits-popup-skip-checkbox}"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<Button Name="ButtonClose"
|
||||
Text="{Loc disabled-traits-popup-close-button}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 10 0 0"
|
||||
MinSize="120 0" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
|
@ -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<ProtoId<TraitPrototype>, List<string>> disabledTraits)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
InitializeUI(disabledTraits);
|
||||
InitializeEvents();
|
||||
}
|
||||
|
||||
private void InitializeUI(Dictionary<ProtoId<TraitPrototype>, List<string>> 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<ProtoId<TraitPrototype>, List<string>> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
/// </summary>
|
||||
public void UpdateConditions(string? jobId, string? speciesId)
|
||||
public void UpdateConditions(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId)
|
||||
{
|
||||
foreach (var (_, entry) in _traitEntries)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,11 +7,27 @@
|
|||
HorizontalExpand="True"
|
||||
Margin="10 8">
|
||||
|
||||
<!-- Checkbox / Toggle -->
|
||||
<CheckBox Name="TraitCheckbox"
|
||||
StyleClasses="TraitsEntryCheckbox"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0 2 10 0"/>
|
||||
<!-- Toggle Container -->
|
||||
<BoxContainer Name="ToggleContainer"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0 2 10 0"
|
||||
SetWidth="24">
|
||||
|
||||
<!-- Checkbox -->
|
||||
<CheckBox Name="TraitCheckbox"
|
||||
StyleClasses="TraitsEntryCheckbox"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<!-- Lock Icon -->
|
||||
<TextureRect Name="LockIcon"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
SetSize="20 20"
|
||||
Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/VerbIcons/lock.svg.192dpi.png"
|
||||
Visible="False"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Trait Info -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
|
|
@ -31,7 +47,6 @@
|
|||
<!-- Description -->
|
||||
<RichTextLabel Name="TraitDescriptionLabel"
|
||||
StyleClasses="TraitsEntryDescriptionLabel"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 4 0 0"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -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<bool>? 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
|
|||
/// <summary>
|
||||
/// Updates whether conditions are met based on current job/species.
|
||||
/// </summary>
|
||||
public void UpdateConditionsMet(string? jobId, string? speciesId)
|
||||
public void UpdateConditionsMet(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? 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<SpeciesPrototype>? 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<JobPrototype>? 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<JobPrototype>? 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<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? 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)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,14 @@ public sealed class TraitsSheetlet<T> : Sheetlet<T> 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<T> : Sheetlet<T> where T : PalettedStylesheet
|
|||
|
||||
E<Button>()
|
||||
.Class("TraitsCategoryHeaderButton")
|
||||
.Prop(Button.StylePropertyStyleBox, categoryHeaderButtonBox),
|
||||
.Prop(ContainerButton.StylePropertyStyleBox, categoryHeaderButtonBox),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsCategoryExpandIcon")
|
||||
|
|
@ -200,10 +208,15 @@ public sealed class TraitsSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet
|
|||
.Panel(entryPanelBox),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsEntryPanel")
|
||||
.Class("TraitsEntrySelected")
|
||||
.Class("TraitsEntryPanel", "TraitsEntrySelected")
|
||||
.Panel(entrySelectedBox),
|
||||
|
||||
// Disabled entry styling
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsEntryPanel", "TraitsEntryDisabled")
|
||||
.Panel(entryDisabledBox)
|
||||
.Modulate(new Color(1f, 1f, 1f, 0.5f)),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsEntryNameLabel")
|
||||
.Font(sheet.BaseFont.GetFont(11))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Linq;
|
||||
using Content.Shared._DV.CCVars;
|
||||
using Content.Shared._DV.Traits;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
|
@ -31,6 +32,8 @@ public sealed partial class TraitsTab : BoxContainer
|
|||
private string _currentSearchText = string.Empty;
|
||||
private bool _awaitingLayoutUpdate;
|
||||
|
||||
private ProtoId<SpeciesPrototype>? _currentSpeciesId;
|
||||
|
||||
public TraitsTab()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
|
@ -261,8 +264,25 @@ public sealed partial class TraitsTab : BoxContainer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates all trait conditions based on current job and species.
|
||||
/// Call this when job or species changes in the character creator.
|
||||
/// </summary>
|
||||
/// <param name="speciesId">Current species ID, or null if none selected</param>
|
||||
public void UpdateConditions(ProtoId<SpeciesPrototype> speciesId)
|
||||
{
|
||||
_currentSpeciesId = speciesId;
|
||||
UpdateAllConditions();
|
||||
}
|
||||
|
||||
private void UpdateAllConditions()
|
||||
{
|
||||
foreach (var (_, categoryUi) in _categoryUis)
|
||||
{
|
||||
// If some fork wants to use the top selected job as well, just add that to the UpdateConditions method in the editor
|
||||
categoryUi.UpdateConditions(null, _currentSpeciesId);
|
||||
}
|
||||
|
||||
RecalculateStats();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -598,7 +598,7 @@ public sealed partial class TraitSystemTest
|
|||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null });
|
||||
new object[] { player, selectedTraits, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
|
|
@ -639,7 +639,7 @@ public sealed partial class TraitSystemTest
|
|||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null });
|
||||
new object[] { player, selectedTraits, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>()});
|
||||
|
||||
Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxTraits limit");
|
||||
|
||||
|
|
@ -673,7 +673,7 @@ public sealed partial class TraitSystemTest
|
|||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null });
|
||||
new object[] { player, selectedTraits, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
|
||||
|
||||
Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxPoints limit");
|
||||
|
||||
|
|
@ -706,7 +706,7 @@ public sealed partial class TraitSystemTest
|
|||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null });
|
||||
new object[] { player, selectedTraits, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
|
||||
|
||||
Assert.That(validTraits?.Contains("TestTraitHasComp"), Is.True, "Trait with met condition should be valid");
|
||||
|
||||
|
|
@ -739,7 +739,7 @@ public sealed partial class TraitSystemTest
|
|||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null });
|
||||
new object[] { player, selectedTraits, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
|
||||
|
||||
Assert.That(validTraits?.Contains("TestTraitHasComp"),
|
||||
Is.False,
|
||||
|
|
|
|||
|
|
@ -50,8 +50,11 @@ public sealed class TraitSystem : EntitySystem
|
|||
if (TryComp<HumanoidAppearanceComponent>(args.Mob, out var humanoid))
|
||||
speciesId = humanoid.Species;
|
||||
|
||||
// Track disabled traits and reasons
|
||||
var disabledTraits = new Dictionary<ProtoId<TraitPrototype>, List<string>>();
|
||||
|
||||
// Validate and collect valid traits
|
||||
var validTraits = ValidateTraits(args.Mob, args.Profile.TraitPreferences, args.Player, args.JobId, speciesId);
|
||||
var validTraits = ValidateTraits(args.Mob, args.Profile.TraitPreferences, args.Player, args.JobId, speciesId, disabledTraits);
|
||||
|
||||
// Apply valid traits
|
||||
foreach (var traitId in validTraits)
|
||||
|
|
@ -61,6 +64,12 @@ public sealed class TraitSystem : EntitySystem
|
|||
|
||||
ApplyTrait(args.Mob, trait);
|
||||
}
|
||||
|
||||
// Send disabled traits notification to client if any were rejected
|
||||
if (disabledTraits.Count > 0)
|
||||
{
|
||||
RaiseNetworkEvent(new DisabledTraitsEvent(disabledTraits), args.Player);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -71,7 +80,8 @@ public sealed class TraitSystem : EntitySystem
|
|||
IReadOnlySet<ProtoId<TraitPrototype>> selectedTraits,
|
||||
ICommonSession? session,
|
||||
string? jobId,
|
||||
string? speciesId)
|
||||
string? speciesId,
|
||||
Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
|
||||
{
|
||||
var validTraits = new HashSet<ProtoId<TraitPrototype>>();
|
||||
var totalPoints = 0;
|
||||
|
|
@ -100,25 +110,31 @@ public sealed class TraitSystem : EntitySystem
|
|||
continue;
|
||||
}
|
||||
|
||||
var rejectionReasons = new List<string>();
|
||||
|
||||
// Check global trait count limit
|
||||
if (traitCount >= _maxTraitCount)
|
||||
{
|
||||
Log.Warning($"Trait {traitId} rejected: global trait count limit ({_maxTraitCount}) exceeded");
|
||||
rejectionReasons.Add(Loc.GetString("disabled-traits-reason-global-limit"));
|
||||
disabledTraits[traitId] = rejectionReasons;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check global points limit
|
||||
if (totalPoints + trait.Cost > _maxTraitPoints)
|
||||
{
|
||||
Log.Warning(
|
||||
$"Trait {traitId} rejected: global points limit ({_maxTraitPoints}) would be exceeded");
|
||||
Log.Warning($"Trait {traitId} rejected: global points limit ({_maxTraitPoints}) would be exceeded");
|
||||
rejectionReasons.Add(Loc.GetString("disabled-traits-reason-points-limit"));
|
||||
disabledTraits[traitId] = rejectionReasons;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check category limits
|
||||
if (!ValidateCategoryLimits(trait, categoryTraitCounts, categoryPointTotals))
|
||||
if (!ValidateCategoryLimits(trait, categoryTraitCounts, categoryPointTotals, rejectionReasons))
|
||||
{
|
||||
Log.Warning($"Trait {traitId} rejected: category limits exceeded");
|
||||
disabledTraits[traitId] = rejectionReasons;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -130,6 +146,11 @@ public sealed class TraitSystem : EntitySystem
|
|||
if (trait.Conflicts.Contains(validTraitId))
|
||||
{
|
||||
Log.Warning($"Trait {traitId} rejected: conflicts with {validTraitId}");
|
||||
if (_prototype.TryIndex(validTraitId, out var conflictTrait))
|
||||
{
|
||||
rejectionReasons.Add(Loc.GetString("disabled-traits-reason-conflict",
|
||||
("trait", Loc.GetString(conflictTrait.Name))));
|
||||
}
|
||||
hasConflict = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -139,18 +160,24 @@ public sealed class TraitSystem : EntitySystem
|
|||
validTrait.Conflicts.Contains(traitId))
|
||||
{
|
||||
Log.Warning($"Trait {traitId} rejected: {validTraitId} conflicts with it");
|
||||
rejectionReasons.Add(Loc.GetString("disabled-traits-reason-conflict",
|
||||
("trait", Loc.GetString(validTrait.Name))));
|
||||
hasConflict = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasConflict)
|
||||
{
|
||||
disabledTraits[traitId] = rejectionReasons;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check all conditions
|
||||
if (!CheckConditions(trait, conditionCtx))
|
||||
if (!CheckConditions(trait, conditionCtx, rejectionReasons))
|
||||
{
|
||||
Log.Warning($"Trait {traitId} rejected: conditions not met");
|
||||
disabledTraits[traitId] = rejectionReasons;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -176,7 +203,8 @@ public sealed class TraitSystem : EntitySystem
|
|||
private bool ValidateCategoryLimits(
|
||||
TraitPrototype trait,
|
||||
Dictionary<ProtoId<TraitCategoryPrototype>, int> categoryTraitCounts,
|
||||
Dictionary<ProtoId<TraitCategoryPrototype>, int> categoryPointTotals)
|
||||
Dictionary<ProtoId<TraitCategoryPrototype>, int> categoryPointTotals,
|
||||
List<string> rejectionReasons)
|
||||
{
|
||||
if (!_prototype.TryIndex(trait.Category, out var category))
|
||||
return true; // Unknown category, allow it
|
||||
|
|
@ -186,11 +214,19 @@ public sealed class TraitSystem : EntitySystem
|
|||
|
||||
// Check category trait count limit
|
||||
if (category.MaxTraits.HasValue && currentCount >= category.MaxTraits.Value)
|
||||
{
|
||||
rejectionReasons.Add(Loc.GetString("disabled-traits-reason-category-limit",
|
||||
("category", Loc.GetString(category.Name))));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check category points limit
|
||||
if (category.MaxPoints.HasValue && currentPoints + trait.Cost > category.MaxPoints.Value)
|
||||
{
|
||||
rejectionReasons.Add(Loc.GetString("disabled-traits-reason-category-points",
|
||||
("category", Loc.GetString(category.Name))));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -198,12 +234,20 @@ public sealed class TraitSystem : EntitySystem
|
|||
/// <summary>
|
||||
/// Checks all conditions on a trait.
|
||||
/// </summary>
|
||||
private bool CheckConditions(TraitPrototype trait, TraitConditionContext ctx)
|
||||
private bool CheckConditions(TraitPrototype trait, TraitConditionContext ctx, List<string> rejectionReasons)
|
||||
{
|
||||
foreach (var condition in trait.Conditions)
|
||||
{
|
||||
if (!condition.Evaluate(ctx))
|
||||
return false;
|
||||
if (condition.Evaluate(ctx))
|
||||
continue;
|
||||
|
||||
// Get human-readable reason from the condition
|
||||
var tooltip = condition.GetTooltip(ctx.Proto, Loc);
|
||||
|
||||
if (!string.IsNullOrEmpty(tooltip))
|
||||
rejectionReasons.Add(tooltip);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -141,6 +141,12 @@ public sealed partial class DCCVars
|
|||
public static readonly CVarDef<int> MaxTraitPoints =
|
||||
CVarDef.Create("traits.max_points", 15, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Whether to skip showing the disabled traits popup when spawning.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> SkipDisabledTraitsPopup =
|
||||
CVarDef.Create("traits.skip_disabled_traits_popup", false, CVar.CLIENT | CVar.ARCHIVE);
|
||||
|
||||
/*
|
||||
* Feedback webhook
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that passes if ANY of the child conditions pass.
|
||||
/// Use this to create "must meet at least one of these requirements" checks.
|
||||
/// </summary>
|
||||
public sealed partial class AnyOfCondition : BaseTraitCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// List of conditions to check. Passes if any condition evaluates to true.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public List<BaseTraitCondition> Conditions = new();
|
||||
|
||||
protected override bool EvaluateImplementation(TraitConditionContext ctx)
|
||||
{
|
||||
// Inversion doesn't make sense for AnyOfCondition - use inverted child conditions instead
|
||||
if (Invert)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"AnyOfCondition does not support Invert. To require none of the conditions, " +
|
||||
"invert the individual child conditions instead.");
|
||||
}
|
||||
|
||||
// Empty list should fail
|
||||
if (Conditions.Count == 0)
|
||||
return false;
|
||||
|
||||
// Return true if ANY condition passes
|
||||
foreach (var condition in Conditions)
|
||||
{
|
||||
if (condition.Evaluate(ctx))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc)
|
||||
{
|
||||
if (Conditions.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var requirements = new List<string>();
|
||||
|
||||
foreach (var condition in Conditions)
|
||||
{
|
||||
var tooltip = condition.GetTooltip(proto, loc);
|
||||
if (!string.IsNullOrEmpty(tooltip))
|
||||
requirements.Add(tooltip);
|
||||
}
|
||||
|
||||
if (requirements.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
// AAAAAAAAAAA
|
||||
var joinedRequirements = string.Join("\n• ", requirements);
|
||||
|
||||
// Handle inversion in tooltip (ANY becomes NONE when inverted)
|
||||
return Loc.GetString("trait-condition-any-of", ("requirements", joinedRequirements));
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,12 @@ public sealed partial class HasCompCondition : BaseTraitCondition
|
|||
[DataField(required: true, customTypeSerializer: typeof(ComponentNameSerializer))]
|
||||
public string Component = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip text to display, if any.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? Tooltip;
|
||||
|
||||
protected override bool EvaluateImplementation(TraitConditionContext ctx)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Component))
|
||||
|
|
@ -35,6 +41,10 @@ public sealed partial class HasCompCondition : BaseTraitCondition
|
|||
|
||||
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc)
|
||||
{
|
||||
// If there's a custom tooltip supplied, use that
|
||||
if (Tooltip is not null)
|
||||
return Loc.GetString(Tooltip);
|
||||
|
||||
// No tooltip for this condition since we're dealing with comps
|
||||
return string.Empty;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._DV.Traits;
|
||||
|
||||
/// <summary>
|
||||
/// Sent from server to client when a player spawns with traits that were disabled due to unmet conditions.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class DisabledTraitsEvent(Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
|
||||
: EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Dictionary mapping disabled trait IDs to lists of reasons why they were disabled.
|
||||
/// </summary>
|
||||
public Dictionary<ProtoId<TraitPrototype>, List<string>> DisabledTraits = disabledTraits;
|
||||
}
|
||||
|
|
@ -5,6 +5,22 @@ trait-editor-search-placeholder = Search traits...
|
|||
trait-editor-footer-hint = Hover over traits for details
|
||||
trait-editor-footer-info = Negative costs grant bonus points
|
||||
|
||||
## Disabled Traits Popup
|
||||
disabled-traits-popup-title = Traits Disabled
|
||||
disabled-traits-popup-label = Traits Disabled
|
||||
disabled-traits-popup-message = Some of your selected traits could not be applied because they did not meet the required conditions.
|
||||
disabled-traits-popup-list-header = The following traits were disabled:
|
||||
disabled-traits-popup-reason = • {$reason}
|
||||
disabled-traits-popup-skip-checkbox = Don't show this again
|
||||
disabled-traits-popup-close-button = Close
|
||||
|
||||
## Disabled Traits Reasons
|
||||
disabled-traits-reason-global-limit = Global trait limit reached
|
||||
disabled-traits-reason-points-limit = Not enough trait points remaining
|
||||
disabled-traits-reason-category-limit = Category "{$category}" trait limit reached
|
||||
disabled-traits-reason-category-points = Category "{$category}" points limit reached
|
||||
disabled-traits-reason-conflict = Conflicts with selected trait: {$trait}
|
||||
|
||||
## Category suffixes
|
||||
trait-category-traits = {$selected} / {$max} traits
|
||||
trait-category-traits-unlimited = {$selected} traits
|
||||
|
|
@ -16,6 +32,10 @@ trait-conditions-tooltip = [bold]Requirements:[/bold]
|
|||
trait-conditions-not-met-tooltip = Requirements not met:
|
||||
{$requirements}
|
||||
|
||||
## Composite conditions
|
||||
trait-condition-any-of = Any of the following must be true:
|
||||
• {$requirements}
|
||||
|
||||
## Species conditions
|
||||
trait-condition-species-is = You must be a [color=yellow]{$species}[/color].
|
||||
trait-condition-species-not = You must not be a [color=yellow]{$species}[/color].
|
||||
|
|
@ -27,3 +47,6 @@ trait-condition-job-not = You must not be a [color={$color}]{$job}[/color].
|
|||
## Department conditions
|
||||
trait-condition-department-is = You must be in the [color={$color}]{$department}[/color] department.
|
||||
trait-condition-department-not = You must not be in the [color={$color}]{$department}[/color] department.
|
||||
|
||||
## HasComp borg conditions
|
||||
trait-condition-borg-not = You must not be a [color=yellow]borg[/color].
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@
|
|||
- !type:HasCompCondition
|
||||
component: BorgChassis
|
||||
invert: true
|
||||
tooltip: trait-condition-borg-not
|
||||
- !type:IsSpeciesCondition
|
||||
species: IPC
|
||||
invert: true
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
- !type:HasCompCondition
|
||||
component: BorgChassis
|
||||
invert: true
|
||||
tooltip: trait-condition-borg-not
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
|
|
@ -168,6 +169,7 @@
|
|||
- !type:HasCompCondition
|
||||
component: BorgChassis
|
||||
invert: true
|
||||
tooltip: trait-condition-borg-not
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
|
|
|
|||
Loading…
Reference in New Issue