real traits system (#5208)
* holy shit? * final touches * this is dumb * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * whatever go my integration tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * whoopsie forgot blacklists * load bearing loc * minor stuff * whoopsie * cool * I LOVE REFLECTION * got a call from the stink department * i love fluent yes * direction changes * waiter more migrations please * typo ops --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Tobias Berger <toby@tobot.dev>
This commit is contained in:
parent
f05e2613df
commit
4364eaa388
|
|
@ -123,10 +123,10 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
|||
_profileEditor.RefreshSpecies();
|
||||
}
|
||||
|
||||
if (obj.WasModified<TraitPrototype>())
|
||||
{
|
||||
_profileEditor.RefreshTraits();
|
||||
}
|
||||
// if (obj.WasModified<TraitPrototype>()) // DeltaV - Refreshed in TraitsTab
|
||||
// {
|
||||
// _profileEditor.RefreshTraits();
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:ui="clr-namespace:Content.Client.Lobby.UI"
|
||||
xmlns:traits="clr-namespace:Content.Client._DV.Traits.UI"
|
||||
HorizontalExpand="True">
|
||||
<!-- Left side -->
|
||||
<BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
|
||||
|
|
@ -133,12 +134,15 @@
|
|||
<BoxContainer Name="AntagList" Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" Margin="10">
|
||||
<!-- Traits -->
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Name="TraitsList" Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
<!-- DV - Traits -->
|
||||
<!-- <BoxContainer Orientation="Vertical" Margin="10"> -->
|
||||
<!-- ~1~ Traits @1@ -->
|
||||
<!-- <ScrollContainer VerticalExpand="True"> -->
|
||||
<!-- <BoxContainer Name="TraitsList" Orientation="Vertical" /> -->
|
||||
<!-- </ScrollContainer> -->
|
||||
<!-- </BoxContainer> -->
|
||||
<traits:TraitsTab Name="Traits"/>
|
||||
<!-- End DV -->
|
||||
<BoxContainer Name="MarkingsTab" Orientation="Vertical" Margin="10">
|
||||
<!-- Markings -->
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ using System.Globalization;
|
|||
using Content.Client._CD.Records.UI;
|
||||
using Content.Shared._CD.Records;
|
||||
// End CD - Character Records
|
||||
using Content.Shared._DV.Traits; // DV - Traits
|
||||
|
||||
namespace Content.Client.Lobby.UI
|
||||
{
|
||||
|
|
@ -180,6 +181,8 @@ namespace Content.Client.Lobby.UI
|
|||
Save?.Invoke();
|
||||
};
|
||||
|
||||
Traits.OnTraitsChanged += OnTraitsSelectionChanged; // DeltaV
|
||||
|
||||
#region Left
|
||||
|
||||
#region Name
|
||||
|
|
@ -239,7 +242,6 @@ namespace Content.Client.Lobby.UI
|
|||
{
|
||||
SpeciesButton.SelectId(args.Id);
|
||||
SetSpecies(_species[args.Id].ID);
|
||||
RefreshTraits(); // DeltaV - Allows for hiding traits
|
||||
UpdateHairPickers();
|
||||
OnSkinColorOnValueChanged();
|
||||
};
|
||||
|
|
@ -462,7 +464,7 @@ namespace Content.Client.Lobby.UI
|
|||
|
||||
TabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
|
||||
|
||||
RefreshTraits();
|
||||
// RefreshTraits(); // DeltaV
|
||||
|
||||
#region Markings
|
||||
|
||||
|
|
@ -515,6 +517,57 @@ namespace Content.Client.Lobby.UI
|
|||
IsDirty = false;
|
||||
}
|
||||
|
||||
// Begin DeltaV - Traits Integration
|
||||
/// <summary>
|
||||
/// Called when trait selection changes in the TraitsTab.
|
||||
/// Updates the profile with the new trait selection.
|
||||
/// </summary>
|
||||
private void OnTraitsSelectionChanged(HashSet<ProtoId<TraitPrototype>> traits)
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
|
||||
// Remove all existing traits - iterate directly over readonly collection
|
||||
foreach (var existingTrait in Profile.TraitPreferences)
|
||||
{
|
||||
Profile = Profile.WithoutTraitPreference(existingTrait, _prototypeManager);
|
||||
}
|
||||
|
||||
// Add newly selected traits
|
||||
foreach (var trait in traits)
|
||||
{
|
||||
Profile = Profile.WithTraitPreference(trait.Id, _prototypeManager);
|
||||
}
|
||||
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the traits tab with the current profile's selected traits.
|
||||
/// </summary>
|
||||
private void UpdateTraitsSelection()
|
||||
{
|
||||
if (Profile is null)
|
||||
{
|
||||
Traits.SetSelectedTraits(new HashSet<ProtoId<TraitPrototype>>());
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert profile's trait preferences (strings) to ProtoId<TraitPrototype>
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>(Profile.TraitPreferences.Count);
|
||||
foreach (var traitId in Profile.TraitPreferences)
|
||||
{
|
||||
// Validate that the trait still exists in prototypes
|
||||
if (_prototypeManager.HasIndex(traitId))
|
||||
{
|
||||
selectedTraits.Add(new ProtoId<TraitPrototype>(traitId));
|
||||
}
|
||||
}
|
||||
|
||||
Traits.SetSelectedTraits(selectedTraits);
|
||||
}
|
||||
// End DeltaV - Traits Integration
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the flavor text editor status.
|
||||
/// </summary>
|
||||
|
|
@ -549,122 +602,122 @@ namespace Content.Client.Lobby.UI
|
|||
/// <summary>
|
||||
/// Refreshes traits selector
|
||||
/// </summary>
|
||||
public void RefreshTraits()
|
||||
{
|
||||
TraitsList.RemoveAllChildren();
|
||||
|
||||
var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
|
||||
TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
|
||||
|
||||
if (traits.Count < 1)
|
||||
{
|
||||
TraitsList.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("humanoid-profile-editor-no-traits"),
|
||||
FontColorOverride = Color.Gray,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup model
|
||||
Dictionary<string, List<string>> traitGroups = new();
|
||||
List<string> defaultTraits = new();
|
||||
traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
|
||||
|
||||
foreach (var trait in traits)
|
||||
{
|
||||
// Begin DeltaV Additions - Species trait exclusion
|
||||
if (Profile?.Species is { } selectedSpecies && trait.ExcludedSpecies.Contains(selectedSpecies))
|
||||
{
|
||||
Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
||||
continue;
|
||||
}
|
||||
// End DeltaV Additions
|
||||
|
||||
if (trait.Category == null)
|
||||
{
|
||||
defaultTraits.Add(trait.ID);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_prototypeManager.HasIndex(trait.Category))
|
||||
continue;
|
||||
|
||||
var group = traitGroups.GetOrNew(trait.Category);
|
||||
group.Add(trait.ID);
|
||||
}
|
||||
|
||||
// Create UI view from model
|
||||
foreach (var (categoryId, categoryTraits) in traitGroups)
|
||||
{
|
||||
TraitCategoryPrototype? category = null;
|
||||
|
||||
if (categoryId != TraitCategoryPrototype.Default)
|
||||
{
|
||||
category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
|
||||
// Label
|
||||
TraitsList.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString(category.Name),
|
||||
Margin = new Thickness(0, 10, 0, 0),
|
||||
StyleClasses = { StyleClass.LabelHeading },
|
||||
});
|
||||
}
|
||||
|
||||
List<TraitPreferenceSelector?> selectors = new();
|
||||
var selectionCount = 0;
|
||||
|
||||
foreach (var traitProto in categoryTraits)
|
||||
{
|
||||
var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
|
||||
var selector = new TraitPreferenceSelector(trait);
|
||||
|
||||
selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
|
||||
if (selector.Preference)
|
||||
selectionCount += trait.Cost;
|
||||
|
||||
selector.PreferenceChanged += preference =>
|
||||
{
|
||||
if (preference)
|
||||
{
|
||||
Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
||||
}
|
||||
|
||||
SetDirty();
|
||||
RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
|
||||
};
|
||||
selectors.Add(selector);
|
||||
}
|
||||
|
||||
// Selection counter
|
||||
if (category is { MaxTraitPoints: >= 0 })
|
||||
{
|
||||
TraitsList.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
|
||||
FontColorOverride = Color.Gray
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var selector in selectors)
|
||||
{
|
||||
if (selector == null)
|
||||
continue;
|
||||
|
||||
if (category is { MaxTraitPoints: >= 0 } &&
|
||||
selector.Cost + selectionCount > category.MaxTraitPoints)
|
||||
{
|
||||
selector.Checkbox.Label.FontColorOverride = Color.Red;
|
||||
}
|
||||
|
||||
TraitsList.AddChild(selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
// public void RefreshTraits()
|
||||
// {
|
||||
// TraitsList.RemoveAllChildren();
|
||||
//
|
||||
// var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
|
||||
// TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
|
||||
//
|
||||
// if (traits.Count < 1)
|
||||
// {
|
||||
// TraitsList.AddChild(new Label
|
||||
// {
|
||||
// Text = Loc.GetString("humanoid-profile-editor-no-traits"),
|
||||
// FontColorOverride = Color.Gray,
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Setup model
|
||||
// Dictionary<string, List<string>> traitGroups = new();
|
||||
// List<string> defaultTraits = new();
|
||||
// traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
|
||||
//
|
||||
// foreach (var trait in traits)
|
||||
// {
|
||||
// // Begin DeltaV Additions - Species trait exclusion
|
||||
// if (Profile?.Species is { } selectedSpecies && trait.ExcludedSpecies.Contains(selectedSpecies))
|
||||
// {
|
||||
// Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
||||
// continue;
|
||||
// }
|
||||
// // End DeltaV Additions
|
||||
//
|
||||
// if (trait.Category == null)
|
||||
// {
|
||||
// defaultTraits.Add(trait.ID);
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// if (!_prototypeManager.HasIndex(trait.Category))
|
||||
// continue;
|
||||
//
|
||||
// var group = traitGroups.GetOrNew(trait.Category);
|
||||
// group.Add(trait.ID);
|
||||
// }
|
||||
//
|
||||
// // Create UI view from model
|
||||
// foreach (var (categoryId, categoryTraits) in traitGroups)
|
||||
// {
|
||||
// TraitCategoryPrototype? category = null;
|
||||
//
|
||||
// if (categoryId != TraitCategoryPrototype.Default)
|
||||
// {
|
||||
// category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
|
||||
// // Label
|
||||
// TraitsList.AddChild(new Label
|
||||
// {
|
||||
// Text = Loc.GetString(category.Name),
|
||||
// Margin = new Thickness(0, 10, 0, 0),
|
||||
// StyleClasses = { StyleClass.LabelHeading },
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// List<TraitPreferenceSelector?> selectors = new();
|
||||
// var selectionCount = 0;
|
||||
//
|
||||
// foreach (var traitProto in categoryTraits)
|
||||
// {
|
||||
// var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
|
||||
// var selector = new TraitPreferenceSelector(trait);
|
||||
//
|
||||
// selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
|
||||
// if (selector.Preference)
|
||||
// selectionCount += trait.Cost;
|
||||
//
|
||||
// selector.PreferenceChanged += preference =>
|
||||
// {
|
||||
// if (preference)
|
||||
// {
|
||||
// Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
||||
// }
|
||||
//
|
||||
// SetDirty();
|
||||
// RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
|
||||
// };
|
||||
// selectors.Add(selector);
|
||||
// }
|
||||
//
|
||||
// // Selection counter
|
||||
// if (category is { MaxTraitPoints: >= 0 })
|
||||
// {
|
||||
// TraitsList.AddChild(new Label
|
||||
// {
|
||||
// Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
|
||||
// FontColorOverride = Color.Gray
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// foreach (var selector in selectors)
|
||||
// {
|
||||
// if (selector == null)
|
||||
// continue;
|
||||
//
|
||||
// if (category is { MaxTraitPoints: >= 0 } &&
|
||||
// selector.Cost + selectionCount > category.MaxTraitPoints)
|
||||
// {
|
||||
// selector.Checkbox.Label.FontColorOverride = Color.Red;
|
||||
// }
|
||||
//
|
||||
// TraitsList.AddChild(selector);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the species selector.
|
||||
|
|
@ -845,11 +898,13 @@ namespace Content.Client.Lobby.UI
|
|||
_recordsTab.Update(profile);
|
||||
// End CD - Character Records
|
||||
|
||||
UpdateTraitsSelection(); // DeltaV - Traits
|
||||
|
||||
RefreshAntags();
|
||||
RefreshJobs();
|
||||
RefreshLoadouts();
|
||||
RefreshSpecies();
|
||||
RefreshTraits();
|
||||
// RefreshTraits(); // DeltaV
|
||||
RefreshFlavorText();
|
||||
ReloadPreview();
|
||||
|
||||
|
|
|
|||
|
|
@ -19,22 +19,24 @@ public sealed partial class TraitPreferenceSelector : Control
|
|||
|
||||
public event Action<bool>? PreferenceChanged;
|
||||
|
||||
public TraitPreferenceSelector(TraitPrototype trait)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var text = trait.Cost != 0 ? $"[{trait.Cost}] " : "";
|
||||
text += Loc.GetString(trait.Name);
|
||||
|
||||
Cost = trait.Cost;
|
||||
Checkbox.Text = text;
|
||||
Checkbox.OnToggled += OnCheckBoxToggled;
|
||||
|
||||
if (trait.Description is { } desc)
|
||||
{
|
||||
Checkbox.ToolTip = Loc.GetString(desc);
|
||||
}
|
||||
}
|
||||
// DeltaV - This whole control is generally unused but the compiler wouldn't compile if I removed it
|
||||
// So I'm just gonna comment this part out
|
||||
// public TraitPreferenceSelector(TraitPrototype trait)
|
||||
// {
|
||||
// RobustXamlLoader.Load(this);
|
||||
//
|
||||
// var text = trait.Cost != 0 ? $"[{trait.Cost}] " : "";
|
||||
// text += Loc.GetString(trait.Name);
|
||||
//
|
||||
// Cost = trait.Cost;
|
||||
// Checkbox.Text = text;
|
||||
// Checkbox.OnToggled += OnCheckBoxToggled;
|
||||
//
|
||||
// if (trait.Description is { } desc)
|
||||
// {
|
||||
// Checkbox.ToolTip = Loc.GetString(desc);
|
||||
// }
|
||||
// }
|
||||
|
||||
private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 0 8">
|
||||
|
||||
<!-- Category Header -->
|
||||
<PanelContainer Name="HeaderPanel" StyleClasses="TraitsCategoryHeader">
|
||||
<Button Name="HeaderButton"
|
||||
StyleClasses="TraitsCategoryHeaderButton"
|
||||
HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" Margin="8 6">
|
||||
<!-- Expand/Collapse Icon -->
|
||||
<Label Name="ExpandIcon"
|
||||
Text="▼"
|
||||
StyleClasses="TraitsCategoryExpandIcon"
|
||||
Margin="0 0 8 0"/>
|
||||
|
||||
<!-- Category Name -->
|
||||
<Label Name="CategoryNameLabel"
|
||||
StyleClasses="TraitsCategoryNameLabel"/>
|
||||
|
||||
<Control HorizontalExpand="True"/>
|
||||
|
||||
<!-- Category Stats -->
|
||||
<Label Name="CategoryStatsLabel"
|
||||
StyleClasses="TraitsCategoryStatsLabel"
|
||||
Margin="0 0 4 0"/>
|
||||
|
||||
<!-- Category Points (if applicable) -->
|
||||
<Label Name="CategoryPointsLabel"
|
||||
StyleClasses="TraitsCategoryPointsLabel"
|
||||
Visible="False"/>
|
||||
</BoxContainer>
|
||||
</Button>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Accent Bar -->
|
||||
<PanelContainer Name="AccentBar"
|
||||
StyleClasses="TraitsCategoryAccent"
|
||||
SetHeight="3"
|
||||
Margin="0 0 0 0"/>
|
||||
|
||||
<!-- Traits Container -->
|
||||
<PanelContainer Name="ContentPanel" StyleClasses="TraitsCategoryContent">
|
||||
<BoxContainer Name="TraitsContainer"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="8 8"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
using System.Linq;
|
||||
using Content.Shared._DV.Traits;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client._DV.Traits.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TraitCategory : BoxContainer
|
||||
{
|
||||
public event Action<ProtoId<TraitPrototype>, bool>? OnTraitToggled;
|
||||
|
||||
private readonly TraitCategoryPrototype _category;
|
||||
private readonly List<TraitPrototype> _allTraits;
|
||||
private readonly Dictionary<ProtoId<TraitPrototype>, TraitEntry> _traitEntries = new();
|
||||
|
||||
private bool _isExpanded;
|
||||
|
||||
public int SelectedCount;
|
||||
public int PointsSpent;
|
||||
|
||||
public TraitCategory(TraitCategoryPrototype category, List<TraitPrototype> traits)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_category = category;
|
||||
_allTraits = traits;
|
||||
_isExpanded = category.DefaultExpanded;
|
||||
|
||||
CategoryNameLabel.Text = Loc.GetString(category.Name);
|
||||
SetAccentColor(category.AccentColor);
|
||||
|
||||
HeaderButton.OnPressed += _ => ToggleExpanded();
|
||||
|
||||
PopulateTraits();
|
||||
UpdateExpandedState();
|
||||
UpdateStats();
|
||||
}
|
||||
|
||||
private void SetAccentColor(Color color)
|
||||
{
|
||||
AccentBar.PanelOverride = new StyleBoxFlat { BackgroundColor = color }; // dumb stylesheet modulation workaround
|
||||
}
|
||||
|
||||
private void PopulateTraits()
|
||||
{
|
||||
TraitsContainer.RemoveAllChildren();
|
||||
_traitEntries.Clear();
|
||||
|
||||
foreach (var trait in _allTraits)
|
||||
{
|
||||
var entry = new TraitEntry(trait);
|
||||
entry.OnToggled += selected => OnTraitEntryToggled(trait.ID, selected);
|
||||
_traitEntries[trait.ID] = entry;
|
||||
TraitsContainer.AddChild(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTraitEntryToggled(ProtoId<TraitPrototype> traitId, bool selected)
|
||||
{
|
||||
OnTraitToggled?.Invoke(traitId, selected);
|
||||
}
|
||||
|
||||
private void ToggleExpanded()
|
||||
{
|
||||
_isExpanded = !_isExpanded;
|
||||
UpdateExpandedState();
|
||||
}
|
||||
|
||||
private void UpdateExpandedState()
|
||||
{
|
||||
ContentPanel.Visible = _isExpanded;
|
||||
ExpandIcon.Text = _isExpanded ? "▼" : "▶";
|
||||
}
|
||||
|
||||
public void UpdateStats()
|
||||
{
|
||||
SelectedCount = _traitEntries.Values.Count(e => e.IsSelected);
|
||||
PointsSpent = _traitEntries.Values
|
||||
.Where(e => e.IsSelected)
|
||||
.Sum(e => e.TraitCost);
|
||||
|
||||
if (_category.MaxTraits.HasValue)
|
||||
{
|
||||
CategoryStatsLabel.Text = Loc.GetString("trait-category-traits",
|
||||
("selected", SelectedCount),
|
||||
("max", _category.MaxTraits.Value));
|
||||
}
|
||||
else
|
||||
{
|
||||
CategoryStatsLabel.Text = Loc.GetString("trait-category-traits-unlimited",
|
||||
("selected", SelectedCount));
|
||||
}
|
||||
|
||||
if (_category.MaxPoints.HasValue)
|
||||
{
|
||||
CategoryPointsLabel.Visible = true;
|
||||
CategoryPointsLabel.Text = Loc.GetString("trait-category-points",
|
||||
("selected", PointsSpent),
|
||||
("max", _category.MaxPoints.Value));
|
||||
}
|
||||
else
|
||||
{
|
||||
CategoryPointsLabel.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTraitSelected(ProtoId<TraitPrototype> traitId, bool selected)
|
||||
{
|
||||
if (_traitEntries.TryGetValue(traitId, out var entry))
|
||||
{
|
||||
entry.SetSelected(selected);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
foreach (var (_, entry) in _traitEntries)
|
||||
{
|
||||
entry.SetSelected(false);
|
||||
}
|
||||
|
||||
SelectedCount = 0;
|
||||
PointsSpent = 0;
|
||||
UpdateStats();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IDs of all currently selected traits in this category.
|
||||
/// </summary>
|
||||
public IEnumerable<ProtoId<TraitPrototype>> GetSelectedTraitIds()
|
||||
{
|
||||
return _traitEntries
|
||||
.Where(kvp => kvp.Value.IsSelected)
|
||||
.Select(kvp => kvp.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters traits based on search text only
|
||||
/// </summary>
|
||||
public void FilterTraits(string searchText)
|
||||
{
|
||||
var hasVisibleTraits = false;
|
||||
|
||||
foreach (var (traitId, entry) in _traitEntries)
|
||||
{
|
||||
var trait = _allTraits.First(t => t.ID == traitId);
|
||||
var name = Loc.GetString(trait.Name);
|
||||
var description = Loc.GetString(trait.Description);
|
||||
|
||||
var matchesSearch = string.IsNullOrEmpty(searchText) ||
|
||||
name.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
|
||||
description.Contains(searchText, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
entry.Visible = matchesSearch;
|
||||
|
||||
if (entry.Visible)
|
||||
hasVisibleTraits = true;
|
||||
}
|
||||
|
||||
// Hide entire category if no traits match search
|
||||
Visible = hasVisibleTraits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
foreach (var (_, entry) in _traitEntries)
|
||||
{
|
||||
entry.UpdateConditionsMet(jobId, speciesId);
|
||||
}
|
||||
|
||||
// Update stats since some traits may have been deselected
|
||||
UpdateStats();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a trait in this category meets its conditions.
|
||||
/// </summary>
|
||||
public bool TraitMeetsConditions(ProtoId<TraitPrototype> traitId)
|
||||
{
|
||||
return _traitEntries.TryGetValue(traitId, out var entry) && entry.MeetsConditions;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
StyleClasses="TraitsEntryPanel"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 0 4">
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="10 8">
|
||||
|
||||
<!-- Checkbox / Toggle -->
|
||||
<CheckBox Name="TraitCheckbox"
|
||||
StyleClasses="TraitsEntryCheckbox"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0 2 10 0"/>
|
||||
|
||||
<!-- Trait Info -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Center"
|
||||
Name="TraitInfo">
|
||||
|
||||
<!-- Name and Cost Row -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="TraitNameLabel"
|
||||
StyleClasses="TraitsEntryNameLabel"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Label Name="TraitCostLabel"
|
||||
StyleClasses="TraitsEntryCostLabel"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Description -->
|
||||
<RichTextLabel Name="TraitDescriptionLabel"
|
||||
StyleClasses="TraitsEntryDescriptionLabel"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 4 0 0"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
using Content.Shared._DV.Traits;
|
||||
using Content.Shared._DV.Traits.Conditions;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._DV.Traits.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TraitEntry : PanelContainer
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
public event Action<bool>? OnToggled;
|
||||
|
||||
public bool IsSelected => TraitCheckbox.Pressed;
|
||||
public int TraitCost { get; }
|
||||
|
||||
private readonly TraitPrototype _trait;
|
||||
private bool _isUpdating;
|
||||
private readonly List<string> _failedConditionTooltips = new();
|
||||
|
||||
public TraitEntry(TraitPrototype trait)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_trait = trait;
|
||||
TraitCost = trait.Cost;
|
||||
|
||||
// Enable mouse events so tooltips work
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
|
||||
TraitNameLabel.Text = Loc.GetString(trait.Name);
|
||||
TraitDescriptionLabel.SetMessage(Loc.GetString(trait.Description));
|
||||
|
||||
// Format cost display
|
||||
var costPrefix = trait.Cost > 0 ? "+" : "";
|
||||
var costColor = trait.Cost > 0 ? "#ff6b6b" : trait.Cost < 0 ? "#6bff6b" : "#888888";
|
||||
TraitCostLabel.Text = $"{costPrefix}{trait.Cost}";
|
||||
TraitCostLabel.ModulateSelfOverride = Color.FromHex(costColor);
|
||||
|
||||
TraitCheckbox.OnToggled += OnCheckboxToggled;
|
||||
|
||||
// Build condition tooltips
|
||||
UpdateConditionTooltips();
|
||||
}
|
||||
|
||||
private void UpdateConditionTooltips()
|
||||
{
|
||||
var tooltips = new List<string>();
|
||||
|
||||
foreach (var condition in _trait.Conditions)
|
||||
{
|
||||
var tooltip = condition.GetTooltip(_prototype, _loc);
|
||||
if (!string.IsNullOrEmpty(tooltip))
|
||||
tooltips.Add(tooltip);
|
||||
}
|
||||
|
||||
if (tooltips.Count > 0)
|
||||
{
|
||||
var tooltipText = Loc.GetString("trait-conditions-tooltip",
|
||||
("requirements", string.Join("\n", tooltips)));
|
||||
|
||||
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
|
||||
}
|
||||
else
|
||||
TooltipSupplier = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a tooltip control that properly parses markup.
|
||||
/// </summary>
|
||||
private static Tooltip CreateMarkupTooltip(string markupText)
|
||||
{
|
||||
var tooltip = new Tooltip();
|
||||
|
||||
// Parse the markup into a FormattedMessage
|
||||
tooltip.SetMessage(FormattedMessage.FromMarkupOrThrow(markupText));
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates whether conditions are met based on current job/species.
|
||||
/// </summary>
|
||||
public void UpdateConditionsMet(string? jobId, string? speciesId)
|
||||
{
|
||||
_failedConditionTooltips.Clear();
|
||||
MeetsConditions = true;
|
||||
|
||||
foreach (var condition in _trait.Conditions)
|
||||
{
|
||||
var result = condition switch
|
||||
{
|
||||
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
|
||||
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
|
||||
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
// Apply inversion
|
||||
result ^= condition.Invert;
|
||||
|
||||
if (!result)
|
||||
{
|
||||
MeetsConditions = false;
|
||||
var tooltip = condition.GetTooltip(_prototype, _loc);
|
||||
if (!string.IsNullOrEmpty(tooltip))
|
||||
_failedConditionTooltips.Add(tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateDisabledState();
|
||||
}
|
||||
|
||||
private bool CheckSpeciesCondition(IsSpeciesCondition condition, string? speciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(speciesId))
|
||||
return false;
|
||||
|
||||
return speciesId == condition.Species.Id;
|
||||
}
|
||||
|
||||
private bool CheckJobCondition(HasJobCondition condition, string? jobId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(jobId))
|
||||
return false;
|
||||
|
||||
return jobId == condition.Job;
|
||||
}
|
||||
|
||||
private bool CheckDepartmentCondition(InDepartmentCondition condition, string? jobId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(jobId))
|
||||
return false;
|
||||
|
||||
if (!_prototype.TryIndex(condition.Department, out var department))
|
||||
return false;
|
||||
|
||||
return department.Roles.Contains(jobId);
|
||||
}
|
||||
|
||||
private void UpdateDisabledState()
|
||||
{
|
||||
TraitCheckbox.Disabled = !MeetsConditions;
|
||||
|
||||
if (!MeetsConditions)
|
||||
{
|
||||
// Deselect if conditions no longer met
|
||||
if (TraitCheckbox.Pressed)
|
||||
{
|
||||
_isUpdating = true;
|
||||
TraitCheckbox.Pressed = false;
|
||||
UpdateSelectedStyle();
|
||||
_isUpdating = false;
|
||||
OnToggled?.Invoke(false);
|
||||
}
|
||||
|
||||
// Show why it's disabled
|
||||
AddStyleClass("TraitsEntryDisabled");
|
||||
|
||||
// Update tooltip to show failed conditions
|
||||
if (_failedConditionTooltips.Count > 0)
|
||||
{
|
||||
var tooltipText = Loc.GetString("trait-conditions-not-met-tooltip",
|
||||
("requirements", string.Join("\n", _failedConditionTooltips)));
|
||||
|
||||
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveStyleClass("TraitsEntryDisabled");
|
||||
UpdateConditionTooltips(); // Reset to normal tooltips
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCheckboxToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return;
|
||||
|
||||
if (!MeetsConditions)
|
||||
{
|
||||
// Prevent selection if conditions not met
|
||||
_isUpdating = true;
|
||||
TraitCheckbox.Pressed = false;
|
||||
_isUpdating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateSelectedStyle();
|
||||
OnToggled?.Invoke(args.Pressed);
|
||||
}
|
||||
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
_isUpdating = true;
|
||||
TraitCheckbox.Pressed = selected && MeetsConditions;
|
||||
UpdateSelectedStyle();
|
||||
_isUpdating = false;
|
||||
}
|
||||
|
||||
public bool MeetsConditions { get; private set; } = true;
|
||||
|
||||
private void UpdateSelectedStyle()
|
||||
{
|
||||
if (TraitCheckbox.Pressed)
|
||||
AddStyleClass("TraitsEntrySelected");
|
||||
else
|
||||
RemoveStyleClass("TraitsEntrySelected");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
using Content.Client.Stylesheets;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Content.Client.Stylesheets.StylesheetHelpers;
|
||||
|
||||
namespace Content.Client._DV.Traits.UI;
|
||||
|
||||
[CommonSheetlet]
|
||||
public sealed class TraitsSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet
|
||||
{
|
||||
public override StyleRule[] GetRules(T sheet, object config)
|
||||
{
|
||||
// Color palette
|
||||
// sorry but the default ColorPalette just sucks in terms of ligher/darker colors
|
||||
var bgDark = Color.FromHex("#1a1a22");
|
||||
var bgMedium = Color.FromHex("#22222a");
|
||||
var bgLight = Color.FromHex("#2a2a35");
|
||||
var bgLighter = Color.FromHex("#32323e");
|
||||
var textPrimary = Color.FromHex("#e0e0e0");
|
||||
var textSecondary = Color.FromHex("#a0a0a0");
|
||||
var textMuted = Color.FromHex("#707070");
|
||||
var accentGreen = Color.FromHex("#4ade80");
|
||||
var accentYellow = Color.FromHex("#fbbf24");
|
||||
var accentRed = Color.FromHex("#f87171");
|
||||
var accentBlue = Color.FromHex("#60a5fa");
|
||||
|
||||
// StyleBoxes
|
||||
var headerPanelBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = bgLight,
|
||||
BorderColor = bgLighter,
|
||||
BorderThickness = new Thickness(0, 0, 0, 1)
|
||||
};
|
||||
headerPanelBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var searchBarBox = new StyleBoxFlat { BackgroundColor = bgMedium };
|
||||
searchBarBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var searchInputBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = bgDark,
|
||||
ContentMarginLeftOverride = 8,
|
||||
ContentMarginRightOverride = 8
|
||||
};
|
||||
|
||||
var footerPanelBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = bgMedium,
|
||||
BorderColor = bgLighter,
|
||||
BorderThickness = new Thickness(0, 1, 0, 0)
|
||||
};
|
||||
|
||||
var categoryHeaderBox = new StyleBoxFlat { BackgroundColor = bgLight };
|
||||
categoryHeaderBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var categoryHeaderButtonBox = new StyleBoxFlat { BackgroundColor = Color.Transparent };
|
||||
categoryHeaderButtonBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var categoryContentBox = new StyleBoxFlat { BackgroundColor = bgMedium };
|
||||
|
||||
var categoryAccentBox = new StyleBoxFlat { BackgroundColor = accentBlue };
|
||||
|
||||
var entryPanelBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = bgLight,
|
||||
BorderColor = bgLighter,
|
||||
BorderThickness = new Thickness(1)
|
||||
};
|
||||
entryPanelBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var entrySelectedBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#2a3a4a"),
|
||||
BorderColor = accentBlue,
|
||||
BorderThickness = new Thickness(1, 1, 1, 1)
|
||||
};
|
||||
entrySelectedBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var progressBarBgBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = bgDark,
|
||||
BorderColor = bgLighter,
|
||||
BorderThickness = new Thickness(1)
|
||||
};
|
||||
|
||||
var progressBarFillFull = new StyleBoxFlat { BackgroundColor = accentGreen };
|
||||
var progressBarFillPartial = new StyleBoxFlat { BackgroundColor = accentYellow };
|
||||
var progressBarFillLow = new StyleBoxFlat { BackgroundColor = accentRed };
|
||||
var progressBarFillEmpty = new StyleBoxFlat { BackgroundColor = bgDark };
|
||||
|
||||
var rules = new List<StyleRule>
|
||||
{
|
||||
// ===== HEADER PANEL =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsHeaderPanel")
|
||||
.Panel(headerPanelBox),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsTitleLabel")
|
||||
.Font(sheet.BaseFont.GetFont(14))
|
||||
.FontColor(textPrimary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsSubtitleLabel")
|
||||
.Font(sheet.BaseFont.GetFont(11))
|
||||
.FontColor(textSecondary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsStatLabel")
|
||||
.Font(sheet.BaseFont.GetFont(12))
|
||||
.FontColor(accentBlue),
|
||||
|
||||
// ===== PROGRESS BAR =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarBg")
|
||||
.Panel(progressBarBgBox),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarFill")
|
||||
.Panel(progressBarFillFull),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarFull")
|
||||
.Panel(progressBarFillFull),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarPartial")
|
||||
.Panel(progressBarFillPartial),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarLow")
|
||||
.Panel(progressBarFillLow),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarEmpty")
|
||||
.Panel(progressBarFillEmpty),
|
||||
|
||||
// ===== SEARCH BAR =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsSearchBar")
|
||||
.Panel(searchBarBox),
|
||||
|
||||
E<LineEdit>()
|
||||
.Class("TraitsSearchInput")
|
||||
.Prop(LineEdit.StylePropertyStyleBox, searchInputBox),
|
||||
|
||||
// ===== FOOTER =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsFooterPanel")
|
||||
.Panel(footerPanelBox),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsFooterText")
|
||||
.Font(sheet.BaseFont.GetFont(10))
|
||||
.FontColor(textMuted),
|
||||
|
||||
// ===== CATEGORY HEADER =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsCategoryHeader")
|
||||
.Panel(categoryHeaderBox),
|
||||
|
||||
E<Button>()
|
||||
.Class("TraitsCategoryHeaderButton")
|
||||
.Prop(Button.StylePropertyStyleBox, categoryHeaderButtonBox),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsCategoryExpandIcon")
|
||||
.Font(sheet.BaseFont.GetFont(10))
|
||||
.FontColor(textSecondary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsCategoryNameLabel")
|
||||
.Font(sheet.BaseFont.GetFont(12))
|
||||
.FontColor(textPrimary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsCategoryStatsLabel")
|
||||
.Font(sheet.BaseFont.GetFont(10))
|
||||
.FontColor(textSecondary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsCategoryPointsLabel")
|
||||
.Font(sheet.BaseFont.GetFont(10))
|
||||
.FontColor(textMuted),
|
||||
|
||||
// ===== CATEGORY ACCENT =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsCategoryAccent")
|
||||
.Panel(categoryAccentBox),
|
||||
|
||||
// ===== CATEGORY CONTENT =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsCategoryContent")
|
||||
.Panel(categoryContentBox),
|
||||
|
||||
// ===== TRAIT ENTRY =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsEntryPanel")
|
||||
.Panel(entryPanelBox),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsEntryPanel")
|
||||
.Class("TraitsEntrySelected")
|
||||
.Panel(entrySelectedBox),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsEntryNameLabel")
|
||||
.Font(sheet.BaseFont.GetFont(11))
|
||||
.FontColor(textPrimary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsEntryCostLabel")
|
||||
.Font(sheet.BaseFont.GetFont(11)),
|
||||
|
||||
E<RichTextLabel>()
|
||||
.Class("TraitsEntryDescriptionLabel")
|
||||
.Font(sheet.BaseFont.GetFont(10))
|
||||
.FontColor(textSecondary),
|
||||
};
|
||||
|
||||
return rules.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
|
||||
<!-- Header Panel with Global Stats -->
|
||||
<PanelContainer StyleClasses="TraitsHeaderPanel" Margin="0 0 0 8">
|
||||
<BoxContainer Orientation="Vertical" Margin="12 10">
|
||||
<!-- Title Row -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 8">
|
||||
<Label Text="{Loc 'trait-editor-title'}"
|
||||
StyleClasses="TraitsTitleLabel"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Label Name="GlobalTraitCountLabel"
|
||||
StyleClasses="TraitsStatLabel"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Points Progress Bar -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4 0 0">
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 4">
|
||||
<Label Text="{Loc 'trait-editor-points-label'}"
|
||||
StyleClasses="TraitsSubtitleLabel"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Label Name="GlobalPointsLabel"
|
||||
StyleClasses="TraitsStatLabel"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Progress Bar Container -->
|
||||
<PanelContainer StyleClasses="TraitsProgressBarBg" SetHeight="12">
|
||||
<PanelContainer Name="GlobalPointsBar"
|
||||
StyleClasses="TraitsProgressBarFill"
|
||||
HorizontalAlignment="Left"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<PanelContainer StyleClasses="TraitsSearchBar" Margin="0 0 0 8">
|
||||
<LineEdit Name="SearchBar"
|
||||
PlaceHolder="{Loc 'trait-editor-search-placeholder'}"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="TraitsSearchInput"
|
||||
Margin="10 8"/>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Categories Container (Scrollable) -->
|
||||
<ScrollContainer VerticalExpand="True"
|
||||
HScrollEnabled="False">
|
||||
<BoxContainer Name="CategoriesContainer"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 4 0"/>
|
||||
</ScrollContainer>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<PanelContainer StyleClasses="TraitsFooterPanel" Margin="0 8 0 0">
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 6">
|
||||
<Label Name="FooterHintLabel"
|
||||
Text="{Loc 'trait-editor-footer-hint'}"
|
||||
StyleClasses="TraitsFooterText"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Label Name="FooterInfoLabel"
|
||||
Text="{Loc 'trait-editor-footer-info'}"
|
||||
StyleClasses="TraitsFooterText"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
using System.Linq;
|
||||
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;
|
||||
|
||||
namespace Content.Client._DV.Traits.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TraitsTab : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when trait selection changes.
|
||||
/// </summary>
|
||||
public event Action<HashSet<ProtoId<TraitPrototype>>>? OnTraitsChanged;
|
||||
|
||||
private readonly Dictionary<ProtoId<TraitCategoryPrototype>, TraitCategory> _categoryUis = new();
|
||||
private readonly HashSet<ProtoId<TraitPrototype>> _selectedTraits = new();
|
||||
|
||||
private int _maxGlobalTraits;
|
||||
private int _maxGlobalPoints;
|
||||
private int _currentTraitCount;
|
||||
private int _currentPointsSpent;
|
||||
|
||||
private string _currentSearchText = string.Empty;
|
||||
private bool _awaitingLayoutUpdate;
|
||||
|
||||
public TraitsTab()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
SearchBar.OnTextChanged += OnSearchTextChanged;
|
||||
_prototype.PrototypesReloaded += OnProtoReload;
|
||||
|
||||
// Subscribe to CVars
|
||||
_cfg.OnValueChanged(DCCVars.MaxTraitCount, OnMaxTraitCountChanged, true);
|
||||
_cfg.OnValueChanged(DCCVars.MaxTraitPoints, OnMaxTraitPointsChanged, true);
|
||||
|
||||
PopulateCategories();
|
||||
UpdateGlobalStats();
|
||||
}
|
||||
|
||||
private void OnMaxTraitCountChanged(int value)
|
||||
{
|
||||
_maxGlobalTraits = value;
|
||||
UpdateGlobalStats();
|
||||
}
|
||||
|
||||
private void OnMaxTraitPointsChanged(int value)
|
||||
{
|
||||
_maxGlobalPoints = value;
|
||||
UpdateGlobalStats();
|
||||
}
|
||||
|
||||
private void OnProtoReload(PrototypesReloadedEventArgs args)
|
||||
{
|
||||
// Don't refresh if control has been disposed
|
||||
if (Disposed)
|
||||
return;
|
||||
|
||||
if (args.WasModified<TraitPrototype>() || args.WasModified<TraitCategoryPrototype>())
|
||||
RefreshTraits();
|
||||
}
|
||||
|
||||
public void RefreshTraits()
|
||||
{
|
||||
PopulateCategories();
|
||||
UpdateGlobalStats();
|
||||
}
|
||||
|
||||
private void PopulateCategories()
|
||||
{
|
||||
CategoriesContainer.RemoveAllChildren();
|
||||
_categoryUis.Clear();
|
||||
|
||||
var categories = _prototype.EnumeratePrototypes<TraitCategoryPrototype>()
|
||||
.OrderBy(c => c.Priority)
|
||||
.ThenBy(c => Loc.GetString(c.Name))
|
||||
.ToList();
|
||||
|
||||
var traitsByCategory = _prototype.EnumeratePrototypes<TraitPrototype>()
|
||||
.GroupBy(t => t.Category)
|
||||
.ToDictionary(g => g.Key, g => g.OrderBy(t => Loc.GetString(t.Name)).ToList());
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (!traitsByCategory.TryGetValue(category.ID, out var traits) || traits.Count == 0)
|
||||
continue;
|
||||
|
||||
var categoryUi = new TraitCategory(category, traits);
|
||||
categoryUi.OnTraitToggled += OnTraitToggled;
|
||||
_categoryUis[category.ID] = categoryUi;
|
||||
CategoriesContainer.AddChild(categoryUi);
|
||||
}
|
||||
|
||||
// Apply current filters and conditions
|
||||
ApplySearchFilter();
|
||||
UpdateAllConditions();
|
||||
}
|
||||
|
||||
private void OnTraitToggled(ProtoId<TraitPrototype> traitId, bool selected)
|
||||
{
|
||||
var trait = _prototype.Index(traitId);
|
||||
|
||||
if (selected)
|
||||
{
|
||||
// Check global limits
|
||||
if (_currentTraitCount >= _maxGlobalTraits)
|
||||
{
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentPointsSpent + trait.Cost > _maxGlobalPoints)
|
||||
{
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check category limits
|
||||
if (_categoryUis.TryGetValue(trait.Category, out var categoryUi))
|
||||
{
|
||||
var categoryProto = _prototype.Index(trait.Category);
|
||||
if (categoryProto.MaxTraits.HasValue &&
|
||||
categoryUi.SelectedCount >= categoryProto.MaxTraits.Value)
|
||||
{
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (categoryProto.MaxPoints.HasValue &&
|
||||
categoryUi.PointsSpent + trait.Cost > categoryProto.MaxPoints.Value)
|
||||
{
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if trait meets conditions
|
||||
if (!categoryUi.TraitMeetsConditions(traitId))
|
||||
{
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check conflicts
|
||||
foreach (var conflict in trait.Conflicts)
|
||||
{
|
||||
if (!_selectedTraits.Contains(conflict))
|
||||
continue;
|
||||
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedTraits.Add(traitId);
|
||||
_currentTraitCount++;
|
||||
_currentPointsSpent += trait.Cost;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedTraits.Remove(traitId);
|
||||
_currentTraitCount--;
|
||||
_currentPointsSpent -= trait.Cost;
|
||||
}
|
||||
|
||||
UpdateGlobalStats();
|
||||
UpdateCategoryStats(trait.Category);
|
||||
OnTraitsChanged?.Invoke(_selectedTraits);
|
||||
}
|
||||
|
||||
private void RevertTraitToggle(ProtoId<TraitPrototype> traitId)
|
||||
{
|
||||
var trait = _prototype.Index(traitId);
|
||||
if (_categoryUis.TryGetValue(trait.Category, out var categoryUi))
|
||||
{
|
||||
categoryUi.SetTraitSelected(traitId, _selectedTraits.Contains(traitId));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGlobalStats()
|
||||
{
|
||||
GlobalTraitCountLabel.Text = $"{_currentTraitCount} / {_maxGlobalTraits}";
|
||||
GlobalPointsLabel.Text = $"{_maxGlobalPoints - _currentPointsSpent} / {_maxGlobalPoints}";
|
||||
|
||||
// Calculate remaining points (clamped to not go below 0 in display)
|
||||
var remainingPoints = _maxGlobalPoints - _currentPointsSpent;
|
||||
GlobalPointsLabel.Text = $"{remainingPoints} / {_maxGlobalPoints}";
|
||||
|
||||
// Calculate progress bar percentage - clamp between 0 and 1
|
||||
var percentage = _maxGlobalPoints > 0
|
||||
? Math.Clamp((float)remainingPoints / _maxGlobalPoints, 0f, 1f)
|
||||
: 0f;
|
||||
|
||||
// Update progress bar using percentage-based sizing
|
||||
var parent = GlobalPointsBar.Parent;
|
||||
if (parent != null)
|
||||
{
|
||||
var parentWidth = parent.Width;
|
||||
// If parent width is 0 (not laid out yet), defer until layout happens
|
||||
if (parentWidth > 0)
|
||||
{
|
||||
GlobalPointsBar.SetWidth = (int)(parentWidth * percentage);
|
||||
_awaitingLayoutUpdate = false;
|
||||
}
|
||||
else if (!_awaitingLayoutUpdate)
|
||||
{
|
||||
// Schedule update after parent layout (only once)
|
||||
_awaitingLayoutUpdate = true;
|
||||
parent.OnResized += OnProgressBarParentResized;
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress bar color class
|
||||
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarFull");
|
||||
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarPartial");
|
||||
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarLow");
|
||||
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarEmpty");
|
||||
|
||||
GlobalPointsBar.AddStyleClass(percentage switch
|
||||
{
|
||||
>= 0.99f => "TraitsProgressBarFull",
|
||||
>= 0.5f => "TraitsProgressBarPartial",
|
||||
> 0f => "TraitsProgressBarLow",
|
||||
_ => "TraitsProgressBarEmpty"
|
||||
});
|
||||
}
|
||||
|
||||
private void OnProgressBarParentResized()
|
||||
{
|
||||
_awaitingLayoutUpdate = false;
|
||||
UpdateGlobalStats();
|
||||
}
|
||||
|
||||
private void UpdateCategoryStats(ProtoId<TraitCategoryPrototype> categoryId)
|
||||
{
|
||||
if (_categoryUis.TryGetValue(categoryId, out var categoryUi))
|
||||
{
|
||||
categoryUi.UpdateStats();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSearchTextChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
_currentSearchText = args.Text.Trim();
|
||||
ApplySearchFilter();
|
||||
}
|
||||
|
||||
private void ApplySearchFilter()
|
||||
{
|
||||
foreach (var (_, categoryUi) in _categoryUis)
|
||||
{
|
||||
categoryUi.FilterTraits(_currentSearchText);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAllConditions()
|
||||
{
|
||||
RecalculateStats();
|
||||
}
|
||||
|
||||
private void RecalculateStats()
|
||||
{
|
||||
_currentTraitCount = 0;
|
||||
_currentPointsSpent = 0;
|
||||
|
||||
// Rebuild selected traits based on what's actually selected in the UI
|
||||
var previouslySelected = new HashSet<ProtoId<TraitPrototype>>(_selectedTraits);
|
||||
_selectedTraits.Clear();
|
||||
|
||||
foreach (var (_, categoryUi) in _categoryUis)
|
||||
{
|
||||
// Get the selected traits from this category
|
||||
var selectedInCategory = categoryUi.GetSelectedTraitIds();
|
||||
|
||||
foreach (var traitId in selectedInCategory)
|
||||
{
|
||||
if (!_prototype.TryIndex(traitId, out var trait))
|
||||
continue;
|
||||
|
||||
_selectedTraits.Add(traitId);
|
||||
_currentTraitCount++;
|
||||
_currentPointsSpent += trait.Cost;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateGlobalStats();
|
||||
foreach (var (categoryId, _) in _categoryUis)
|
||||
{
|
||||
UpdateCategoryStats(categoryId);
|
||||
}
|
||||
|
||||
// Fire event if selection changed
|
||||
if (!_selectedTraits.SetEquals(previouslySelected))
|
||||
{
|
||||
OnTraitsChanged?.Invoke(_selectedTraits);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the currently selected traits (e.g., when loading a profile).
|
||||
/// </summary>
|
||||
public void SetSelectedTraits(IEnumerable<ProtoId<TraitPrototype>> traits)
|
||||
{
|
||||
// Clear current selection
|
||||
foreach (var (_, categoryUi) in _categoryUis)
|
||||
{
|
||||
categoryUi.ClearSelection();
|
||||
}
|
||||
|
||||
_selectedTraits.Clear();
|
||||
_currentTraitCount = 0;
|
||||
_currentPointsSpent = 0;
|
||||
|
||||
// Apply new selection
|
||||
foreach (var traitId in traits)
|
||||
{
|
||||
if (!_prototype.TryIndex(traitId, out var trait))
|
||||
continue;
|
||||
|
||||
_selectedTraits.Add(traitId);
|
||||
_currentTraitCount++;
|
||||
_currentPointsSpent += trait.Cost;
|
||||
|
||||
if (_categoryUis.TryGetValue(trait.Category, out var categoryUi))
|
||||
{
|
||||
categoryUi.SetTraitSelected(traitId, true);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateGlobalStats();
|
||||
foreach (var (categoryId, _) in _categoryUis)
|
||||
{
|
||||
UpdateCategoryStats(categoryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,797 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Content.Server._DV.Traits;
|
||||
using Content.Shared._DV.Traits;
|
||||
using Content.Shared._DV.Traits.Conditions;
|
||||
using Content.Shared._DV.Traits.Effects;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests._DV;
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive integration tests for the trait system.
|
||||
/// Tests all conditions, effects, and validation logic.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[TestOf(typeof(TraitSystemTest))]
|
||||
public sealed partial class TraitSystemTest
|
||||
{
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
# Test Trait Categories
|
||||
- type: traitCategory
|
||||
id: TestCategoryUnlimited
|
||||
name: trait-dysgraphia-name
|
||||
maxTraits: null
|
||||
maxPoints: null
|
||||
|
||||
- type: traitCategory
|
||||
id: TestCategoryLimited
|
||||
name: trait-dysgraphia-name
|
||||
maxTraits: 2
|
||||
maxPoints: 10
|
||||
|
||||
# Test Traits - Conditions
|
||||
- type: trait
|
||||
id: TestTraitHasComp
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
conditions:
|
||||
- !type:HasCompCondition
|
||||
component: Hunger
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
|
||||
|
||||
# Test Traits - Effects
|
||||
- type: trait
|
||||
id: TestTraitAddComps
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
- type: Hunger
|
||||
|
||||
- type: trait
|
||||
id: TestTraitOverrideComps
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:OverrideCompsEffect
|
||||
components:
|
||||
- type: Hunger
|
||||
|
||||
- type: trait
|
||||
id: TestTraitRemComps
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:RemCompsEffect
|
||||
components:
|
||||
- Hunger
|
||||
- Thirst
|
||||
|
||||
- type: trait
|
||||
id: TestTraitSpawnItem
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:SpawnItemInHandEffect
|
||||
item: Pen
|
||||
|
||||
# Test Traits - Validation
|
||||
- type: trait
|
||||
id: TestTraitConflictA
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
conflicts:
|
||||
- TestTraitConflictB
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
|
||||
- type: trait
|
||||
id: TestTraitConflictB
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
|
||||
- type: trait
|
||||
id: TestTraitLimited1
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryLimited
|
||||
cost: 5
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
|
||||
- type: trait
|
||||
id: TestTraitLimited2
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryLimited
|
||||
cost: 5
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
|
||||
- type: trait
|
||||
id: TestTraitLimited3
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryLimited
|
||||
cost: 5
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
";
|
||||
|
||||
#region Condition Tests
|
||||
|
||||
[RegisterComponent]
|
||||
private sealed partial class TestComponent : Component;
|
||||
|
||||
[Test]
|
||||
public async Task HasCompCondition_WithComponent_ReturnsTrue()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
entMan.AddComponent<HungerComponent>(player);
|
||||
|
||||
var condition = new HasCompCondition { Component = "Hunger" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player);
|
||||
|
||||
Assert.That(condition.Evaluate(ctx), Is.True, "HasCompCondition should return true when component exists");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task HasCompCondition_WithoutComponent_ReturnsFalse()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new HasCompCondition { Component = "Hunger" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player);
|
||||
|
||||
Assert.That(condition.Evaluate(ctx),
|
||||
Is.False,
|
||||
"HasCompCondition should return false when component doesn't exist");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task HasCompCondition_Inverted_ReturnsOpposite()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
entMan.AddComponent<HungerComponent>(player);
|
||||
|
||||
var condition = new HasCompCondition { Component = "Hunger", Invert = true };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player);
|
||||
|
||||
Assert.That(condition.Evaluate(ctx),
|
||||
Is.False,
|
||||
"Inverted HasCompCondition should return false when component exists");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task HasJobCondition_MatchingJob_ReturnsTrue()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new HasJobCondition { Job = "MedicalDoctor" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, "MedicalDoctor");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx), Is.True, "HasJobCondition should return true for matching job");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task HasJobCondition_DifferentJob_ReturnsFalse()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new HasJobCondition { Job = "MedicalDoctor" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, "SecurityOfficer");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx), Is.False, "HasJobCondition should return false for different job");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task InDepartmentCondition_JobInDepartment_ReturnsTrue()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new InDepartmentCondition { Department = "Medical" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, "MedicalDoctor");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx),
|
||||
Is.True,
|
||||
"InDepartmentCondition should return true when job is in department");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task InDepartmentCondition_JobNotInDepartment_ReturnsFalse()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new InDepartmentCondition { Department = "Medical" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, "SecurityOfficer");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx),
|
||||
Is.False,
|
||||
"InDepartmentCondition should return false when job is not in department");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsSpeciesCondition_MatchingSpecies_ReturnsTrue()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new IsSpeciesCondition { Species = "Human" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, speciesId: "Human");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx), Is.True, "IsSpeciesCondition should return true for matching species");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsSpeciesCondition_DifferentSpecies_ReturnsFalse()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new IsSpeciesCondition { Species = "Human" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, speciesId: "Vox");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx),
|
||||
Is.False,
|
||||
"IsSpeciesCondition should return false for different species");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Effect Tests
|
||||
|
||||
[Test]
|
||||
public async Task AddCompsEffect_AddsComponents()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
Assert.That(entMan.HasComponent<HungerComponent>(player),
|
||||
Is.False,
|
||||
"Player should not start with HungerComponent");
|
||||
|
||||
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitAddComps"));
|
||||
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
|
||||
|
||||
foreach (var effect in trait.Effects)
|
||||
{
|
||||
effect.Apply(ctx);
|
||||
}
|
||||
|
||||
Assert.That(entMan.HasComponent<HungerComponent>(player),
|
||||
Is.True,
|
||||
"AddCompsEffect should add HungerComponent");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AddCompsEffect_DoesNotOverwrite()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
var hungerBefore = entMan.AddComponent<HungerComponent>(player);
|
||||
|
||||
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitAddComps"));
|
||||
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
|
||||
|
||||
foreach (var effect in trait.Effects)
|
||||
{
|
||||
effect.Apply(ctx);
|
||||
}
|
||||
|
||||
var hungerAfter = entMan.GetComponent<HungerComponent>(player);
|
||||
Assert.That(hungerAfter,
|
||||
Is.SameAs(hungerBefore),
|
||||
"AddCompsEffect should not replace existing component instance");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OverrideCompsEffect_OverwritesComponent()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
var hungerBefore = entMan.AddComponent<HungerComponent>(player);
|
||||
|
||||
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitOverrideComps"));
|
||||
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
|
||||
|
||||
foreach (var effect in trait.Effects)
|
||||
{
|
||||
effect.Apply(ctx);
|
||||
}
|
||||
|
||||
var hungerAfter = entMan.GetComponent<HungerComponent>(player);
|
||||
Assert.That(hungerAfter,
|
||||
Is.Not.SameAs(hungerBefore),
|
||||
"OverrideCompsEffect should replace existing component instance");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RemCompsEffect_RemovesComponents()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
entMan.AddComponent<HungerComponent>(player);
|
||||
entMan.AddComponent<ThirstComponent>(player);
|
||||
|
||||
Assert.That(entMan.HasComponent<HungerComponent>(player),
|
||||
Is.True,
|
||||
"Player should start with HungerComponent");
|
||||
Assert.That(entMan.HasComponent<ThirstComponent>(player),
|
||||
Is.True,
|
||||
"Player should start with ThirstComponent");
|
||||
|
||||
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitRemComps"));
|
||||
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
|
||||
|
||||
foreach (var effect in trait.Effects)
|
||||
{
|
||||
effect.Apply(ctx);
|
||||
}
|
||||
|
||||
Assert.That(entMan.HasComponent<HungerComponent>(player),
|
||||
Is.False,
|
||||
"RemCompsEffect should remove HungerComponent");
|
||||
Assert.That(entMan.HasComponent<ThirstComponent>(player),
|
||||
Is.False,
|
||||
"RemCompsEffect should remove ThirstComponent");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SpawnItemInHandEffect_SpawnsItem()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity("MobHuman", MapCoordinates.Nullspace);
|
||||
var handsSys = entMan.System<SharedHandsSystem>();
|
||||
var hands = entMan.GetComponent<HandsComponent>(player);
|
||||
|
||||
Assert.That(handsSys.GetActiveItem((player, hands)), Is.Null, "Player should start with empty hands");
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitSpawnItem"));
|
||||
|
||||
// We need to use reflection to call the private ApplyTrait method
|
||||
var method = typeof(TraitSystem).GetMethod("ApplyTrait",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
method?.Invoke(traitSys, new object[] { player, trait });
|
||||
|
||||
var item = handsSys.GetActiveItem((player, hands));
|
||||
Assert.That(item, Is.Not.Null, "SpawnItemInHandEffect should spawn item in hand");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validation Tests
|
||||
|
||||
[Test]
|
||||
public async Task RespectsConflicts()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
|
||||
{
|
||||
"TestTraitConflictA",
|
||||
"TestTraitConflictB",
|
||||
};
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(validTraits?.Count, Is.EqualTo(1), "Only one conflicting trait should be valid");
|
||||
Assert.That(validTraits.Contains("TestTraitConflictA"), Is.True, "First trait should be kept");
|
||||
Assert.That(validTraits.Contains("TestTraitConflictB"),
|
||||
Is.False,
|
||||
"Conflicting trait should be rejected");
|
||||
});
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RespectsCategoryLimits()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
// TestCategoryLimited has maxTraits: 2
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
|
||||
{
|
||||
"TestTraitLimited1",
|
||||
"TestTraitLimited2",
|
||||
"TestTraitLimited3",
|
||||
};
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null });
|
||||
|
||||
Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxTraits limit");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RespectsCategoryPointLimits()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
// TestCategoryLimited has maxPoints: 10, each trait costs 5
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
|
||||
{
|
||||
"TestTraitLimited1",
|
||||
"TestTraitLimited2",
|
||||
"TestTraitLimited3", // This would exceed the 10 point limit
|
||||
};
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null });
|
||||
|
||||
Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxPoints limit");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChecksConditionsOnSpawn()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
entMan.AddComponent<HungerComponent>(player);
|
||||
|
||||
// Trait requires HungerComponent
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
|
||||
{
|
||||
"TestTraitHasComp",
|
||||
};
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null });
|
||||
|
||||
Assert.That(validTraits?.Contains("TestTraitHasComp"), Is.True, "Trait with met condition should be valid");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RejectsTraitsWithUnmetConditions()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
// Player does NOT have HungerComponent
|
||||
|
||||
// Trait requires HungerComponent
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
|
||||
{
|
||||
"TestTraitHasComp",
|
||||
};
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null });
|
||||
|
||||
Assert.That(validTraits?.Contains("TestTraitHasComp"),
|
||||
Is.False,
|
||||
"Trait with unmet condition should be rejected");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static TraitConditionContext CreateContext(
|
||||
IEntityManager entMan,
|
||||
IPrototypeManager protoMan,
|
||||
IComponentFactory factory,
|
||||
EntityUid player,
|
||||
string? jobId = null,
|
||||
string? speciesId = null)
|
||||
{
|
||||
return new TraitConditionContext
|
||||
{
|
||||
Player = player,
|
||||
Session = null,
|
||||
EntMan = entMan,
|
||||
Proto = protoMan,
|
||||
CompFactory = factory,
|
||||
LogMan = IoCManager.Resolve<ILogManager>(),
|
||||
JobId = jobId,
|
||||
SpeciesId = speciesId,
|
||||
};
|
||||
}
|
||||
|
||||
private static TraitEffectContext CreateEffectContext(
|
||||
IEntityManager entMan,
|
||||
IPrototypeManager protoMan,
|
||||
IComponentFactory factory,
|
||||
EntityUid player)
|
||||
{
|
||||
return new TraitEffectContext
|
||||
{
|
||||
Player = player,
|
||||
EntMan = entMan,
|
||||
Proto = protoMan,
|
||||
CompFactory = factory,
|
||||
LogMan = IoCManager.Resolve<ILogManager>(),
|
||||
Transform = entMan.GetComponent<TransformComponent>(player),
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
2273
Content.Server.Database/Migrations/Postgres/20260117145027_TruncateTraitTable.Designer.cs
generated
Normal file
2273
Content.Server.Database/Migrations/Postgres/20260117145027_TruncateTraitTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,22 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class TruncateTraitTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("TRUNCATE TABLE trait RESTART IDENTITY;");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Not like truncate operations can be reversed
|
||||
}
|
||||
}
|
||||
}
|
||||
2191
Content.Server.Database/Migrations/Sqlite/20260117145007_TruncateTraitTable.Designer.cs
generated
Normal file
2191
Content.Server.Database/Migrations/Sqlite/20260117145007_TruncateTraitTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,25 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class TruncateTraitTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
DELETE FROM trait;
|
||||
DELETE FROM sqlite_sequence WHERE name='trait';
|
||||
");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Cannot reverse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ using Robust.Shared.Map;
|
|||
using Robust.Shared.Prototypes;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Traits.Assorted; // DV
|
||||
|
||||
namespace Content.Server.Cloning;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ using Robust.Shared.Utility;
|
|||
using Content.Server._CD.Records; // CD - Character Records
|
||||
using Content.Shared._CD.Records; // CD - Character Records
|
||||
using Content.Shared._DV.Tips; // DV - Tips
|
||||
using Content.Shared._DV.Traits; // DV - Traits
|
||||
|
||||
namespace Content.Server.Database
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
namespace Content.Server.Traits.Assorted;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for the uncloneable trait.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class UncloneableComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -1,70 +1,70 @@
|
|||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Traits;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Traits;
|
||||
|
||||
public sealed class TraitSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete);
|
||||
}
|
||||
|
||||
// When the player is spawned in, add all trait components selected during character creation
|
||||
private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
|
||||
{
|
||||
// Check if player's job allows to apply traits
|
||||
if (args.JobId == null ||
|
||||
!_prototypeManager.Resolve<JobPrototype>(args.JobId, out var protoJob) ||
|
||||
!protoJob.ApplyTraits)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var traitId in args.Profile.TraitPreferences)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<TraitPrototype>(traitId, out var traitPrototype))
|
||||
{
|
||||
Log.Error($"No trait found with ID {traitId}!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_whitelistSystem.IsWhitelistFail(traitPrototype.Whitelist, args.Mob) ||
|
||||
_whitelistSystem.IsBlacklistPass(traitPrototype.Blacklist, args.Mob))
|
||||
continue;
|
||||
|
||||
// Add all components required by the prototype
|
||||
EntityManager.AddComponents(args.Mob, traitPrototype.Components, false);
|
||||
|
||||
// Begin DeltaV - Add overridden components
|
||||
if(traitPrototype.OverriddenComponents != null)
|
||||
EntityManager.AddComponents(args.Mob, traitPrototype.OverriddenComponents, true);
|
||||
// End DeltaV
|
||||
|
||||
// Add item required by the trait
|
||||
if (traitPrototype.TraitGear == null)
|
||||
continue;
|
||||
|
||||
if (!TryComp(args.Mob, out HandsComponent? handsComponent))
|
||||
continue;
|
||||
|
||||
var coords = Transform(args.Mob).Coordinates;
|
||||
var inhandEntity = Spawn(traitPrototype.TraitGear, coords);
|
||||
_sharedHandsSystem.TryPickup(args.Mob,
|
||||
inhandEntity,
|
||||
checkActionBlocker: false,
|
||||
handsComp: handsComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
// using Content.Shared.GameTicking; // DeltaV - Traits rework
|
||||
// using Content.Shared.Hands.Components;
|
||||
// using Content.Shared.Hands.EntitySystems;
|
||||
// using Content.Shared.Roles;
|
||||
// using Content.Shared.Traits;
|
||||
// using Content.Shared.Whitelist;
|
||||
// using Robust.Shared.Prototypes;
|
||||
//
|
||||
// namespace Content.Server.Traits;
|
||||
//
|
||||
// public sealed class TraitSystem : EntitySystem
|
||||
// {
|
||||
// [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
// [Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!;
|
||||
// [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
//
|
||||
// public override void Initialize()
|
||||
// {
|
||||
// base.Initialize();
|
||||
//
|
||||
// SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete);
|
||||
// }
|
||||
//
|
||||
// // When the player is spawned in, add all trait components selected during character creation
|
||||
// private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
|
||||
// {
|
||||
// // Check if player's job allows to apply traits
|
||||
// if (args.JobId == null ||
|
||||
// !_prototypeManager.Resolve<JobPrototype>(args.JobId, out var protoJob) ||
|
||||
// !protoJob.ApplyTraits)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// foreach (var traitId in args.Profile.TraitPreferences)
|
||||
// {
|
||||
// if (!_prototypeManager.TryIndex<TraitPrototype>(traitId, out var traitPrototype))
|
||||
// {
|
||||
// Log.Error($"No trait found with ID {traitId}!");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if (_whitelistSystem.IsWhitelistFail(traitPrototype.Whitelist, args.Mob) ||
|
||||
// _whitelistSystem.IsBlacklistPass(traitPrototype.Blacklist, args.Mob))
|
||||
// continue;
|
||||
//
|
||||
// // Add all components required by the prototype
|
||||
// EntityManager.AddComponents(args.Mob, traitPrototype.Components, false);
|
||||
//
|
||||
// // Begin DeltaV - Add overridden components
|
||||
// if(traitPrototype.OverriddenComponents != null)
|
||||
// EntityManager.AddComponents(args.Mob, traitPrototype.OverriddenComponents, true);
|
||||
// // End DeltaV
|
||||
//
|
||||
// // Add item required by the trait
|
||||
// if (traitPrototype.TraitGear == null)
|
||||
// continue;
|
||||
//
|
||||
// if (!TryComp(args.Mob, out HandsComponent? handsComponent))
|
||||
// continue;
|
||||
//
|
||||
// var coords = Transform(args.Mob).Coordinates;
|
||||
// var inhandEntity = Spawn(traitPrototype.TraitGear, coords);
|
||||
// _sharedHandsSystem.TryPickup(args.Mob,
|
||||
// inhandEntity,
|
||||
// checkActionBlocker: false,
|
||||
// handsComp: handsComponent);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,263 @@
|
|||
using Content.Shared._DV.CCVars;
|
||||
using Content.Shared._DV.Traits;
|
||||
using Content.Shared._DV.Traits.Conditions;
|
||||
using Content.Shared._DV.Traits.Effects;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._DV.Traits;
|
||||
|
||||
/// <summary>
|
||||
/// Server system that validates and applies traits to players on spawn.
|
||||
/// </summary>
|
||||
public sealed class TraitSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IComponentFactory _factory = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly ILogManager _log = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
|
||||
private int _maxTraitCount;
|
||||
private int _maxTraitPoints;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete);
|
||||
|
||||
Subs.CVar(_config, DCCVars.MaxTraitCount, value => _maxTraitCount = value, true);
|
||||
Subs.CVar(_config, DCCVars.MaxTraitPoints, value => _maxTraitPoints = value, true);
|
||||
}
|
||||
|
||||
private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
|
||||
{
|
||||
// Check if player's job allows traits
|
||||
if (args.JobId == null ||
|
||||
!_prototype.TryIndex<JobPrototype>(args.JobId, out var jobProto) ||
|
||||
!jobProto.ApplyTraits)
|
||||
return;
|
||||
|
||||
// Get species ID for condition checking
|
||||
string? speciesId = null;
|
||||
if (TryComp<HumanoidAppearanceComponent>(args.Mob, out var humanoid))
|
||||
speciesId = humanoid.Species;
|
||||
|
||||
// Validate and collect valid traits
|
||||
var validTraits = ValidateTraits(args.Mob, args.Profile.TraitPreferences, args.Player, args.JobId, speciesId);
|
||||
|
||||
// Apply valid traits
|
||||
foreach (var traitId in validTraits)
|
||||
{
|
||||
if (!_prototype.TryIndex(traitId, out var trait))
|
||||
continue;
|
||||
|
||||
ApplyTrait(args.Mob, trait);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a set of trait selections against all rules and returns the valid subset.
|
||||
/// </summary>
|
||||
private HashSet<ProtoId<TraitPrototype>> ValidateTraits(
|
||||
EntityUid player,
|
||||
IReadOnlySet<ProtoId<TraitPrototype>> selectedTraits,
|
||||
ICommonSession? session,
|
||||
string? jobId,
|
||||
string? speciesId)
|
||||
{
|
||||
var validTraits = new HashSet<ProtoId<TraitPrototype>>();
|
||||
var totalPoints = 0;
|
||||
var traitCount = 0;
|
||||
var categoryTraitCounts = new Dictionary<ProtoId<TraitCategoryPrototype>, int>();
|
||||
var categoryPointTotals = new Dictionary<ProtoId<TraitCategoryPrototype>, int>();
|
||||
|
||||
// Build condition context
|
||||
var conditionCtx = new TraitConditionContext
|
||||
{
|
||||
Player = player,
|
||||
Session = session,
|
||||
EntMan = EntityManager,
|
||||
Proto = _prototype,
|
||||
CompFactory = _factory,
|
||||
LogMan = _log,
|
||||
JobId = jobId,
|
||||
SpeciesId = speciesId,
|
||||
};
|
||||
|
||||
foreach (var traitId in selectedTraits)
|
||||
{
|
||||
if (!_prototype.TryIndex(traitId, out var trait))
|
||||
{
|
||||
Log.Warning($"Unknown trait ID in player preferences: {traitId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check global trait count limit
|
||||
if (traitCount >= _maxTraitCount)
|
||||
{
|
||||
Log.Warning($"Trait {traitId} rejected: global trait count limit ({_maxTraitCount}) exceeded");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check global points limit
|
||||
if (totalPoints + trait.Cost > _maxTraitPoints)
|
||||
{
|
||||
Log.Warning(
|
||||
$"Trait {traitId} rejected: global points limit ({_maxTraitPoints}) would be exceeded");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check category limits
|
||||
if (!ValidateCategoryLimits(trait, categoryTraitCounts, categoryPointTotals))
|
||||
{
|
||||
Log.Warning($"Trait {traitId} rejected: category limits exceeded");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check conflicts with already selected traits
|
||||
var hasConflict = false;
|
||||
foreach (var validTraitId in validTraits)
|
||||
{
|
||||
// Check if current trait conflicts with valid trait
|
||||
if (trait.Conflicts.Contains(validTraitId))
|
||||
{
|
||||
Log.Warning($"Trait {traitId} rejected: conflicts with {validTraitId}");
|
||||
hasConflict = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if valid trait conflicts with current trait
|
||||
if (_prototype.TryIndex(validTraitId, out var validTrait) &&
|
||||
validTrait.Conflicts.Contains(traitId))
|
||||
{
|
||||
Log.Warning($"Trait {traitId} rejected: {validTraitId} conflicts with it");
|
||||
hasConflict = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasConflict)
|
||||
continue;
|
||||
|
||||
// Check all conditions
|
||||
if (!CheckConditions(trait, conditionCtx))
|
||||
{
|
||||
Log.Warning($"Trait {traitId} rejected: conditions not met");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trait is valid, add it
|
||||
validTraits.Add(traitId);
|
||||
totalPoints += trait.Cost;
|
||||
traitCount++;
|
||||
|
||||
// Update category tracking
|
||||
categoryTraitCounts.TryGetValue(trait.Category, out var catCount);
|
||||
categoryTraitCounts[trait.Category] = catCount + 1;
|
||||
|
||||
categoryPointTotals.TryGetValue(trait.Category, out var catPoints);
|
||||
categoryPointTotals[trait.Category] = catPoints + trait.Cost;
|
||||
}
|
||||
|
||||
return validTraits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that adding a trait wouldn't exceed category-specific limits.
|
||||
/// </summary>
|
||||
private bool ValidateCategoryLimits(
|
||||
TraitPrototype trait,
|
||||
Dictionary<ProtoId<TraitCategoryPrototype>, int> categoryTraitCounts,
|
||||
Dictionary<ProtoId<TraitCategoryPrototype>, int> categoryPointTotals)
|
||||
{
|
||||
if (!_prototype.TryIndex(trait.Category, out var category))
|
||||
return true; // Unknown category, allow it
|
||||
|
||||
categoryTraitCounts.TryGetValue(trait.Category, out var currentCount);
|
||||
categoryPointTotals.TryGetValue(trait.Category, out var currentPoints);
|
||||
|
||||
// Check category trait count limit
|
||||
if (category.MaxTraits.HasValue && currentCount >= category.MaxTraits.Value)
|
||||
return false;
|
||||
|
||||
// Check category points limit
|
||||
if (category.MaxPoints.HasValue && currentPoints + trait.Cost > category.MaxPoints.Value)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks all conditions on a trait.
|
||||
/// </summary>
|
||||
private bool CheckConditions(TraitPrototype trait, TraitConditionContext ctx)
|
||||
{
|
||||
foreach (var condition in trait.Conditions)
|
||||
{
|
||||
if (!condition.Evaluate(ctx))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a trait's effects to an entity.
|
||||
/// </summary>
|
||||
private void ApplyTrait(EntityUid player, TraitPrototype trait)
|
||||
{
|
||||
var transform = Transform(player);
|
||||
|
||||
var effectCtx = new TraitEffectContext
|
||||
{
|
||||
Player = player,
|
||||
EntMan = EntityManager,
|
||||
Proto = _prototype,
|
||||
CompFactory = _factory,
|
||||
LogMan = _log,
|
||||
Transform = transform,
|
||||
};
|
||||
|
||||
foreach (var effect in trait.Effects)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Handle SpawnItemInHandEffect specially since it needs server-side systems
|
||||
if (effect is SpawnItemInHandEffect spawnEffect)
|
||||
ApplySpawnItemEffect(player, spawnEffect, transform);
|
||||
else
|
||||
effect.Apply(effectCtx);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Error applying effect {effect.GetType().Name} for trait {trait.ID}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the SpawnItemInHandEffect since it requires server-side systems.
|
||||
/// </summary>
|
||||
private void ApplySpawnItemEffect(EntityUid player, SpawnItemInHandEffect effect, TransformComponent transform)
|
||||
{
|
||||
if (!TryComp<HandsComponent>(player, out var hands))
|
||||
{
|
||||
Log.Warning("Cannot spawn trait item: player has no hands component");
|
||||
return;
|
||||
}
|
||||
|
||||
var coords = transform.Coordinates;
|
||||
var item = Spawn(effect.Item, coords);
|
||||
|
||||
if (!_hands.TryPickup(player, item, checkActionBlocker: false, handsComp: hands))
|
||||
Log.Debug($"Could not pick up trait item {effect.Item}, leaving at feet");
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ using Robust.Shared.Random;
|
|||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Shared._CD.Records; // CD - Character Records
|
||||
using Content.Shared._DV.Traits; // DeltaV - Traits rework
|
||||
|
||||
namespace Content.Shared.Preferences
|
||||
{
|
||||
|
|
@ -444,12 +445,12 @@ namespace Content.Shared.Preferences
|
|||
// Category not found so dump it.
|
||||
TraitCategoryPrototype? traitCategory = null;
|
||||
|
||||
if (category != null && !protoManager.Resolve(category, out traitCategory))
|
||||
if (!protoManager.Resolve(category, out traitCategory)) // DeltaV 13/01/26 - Traits: Category is no longer nullable
|
||||
return new(this);
|
||||
|
||||
var list = new HashSet<ProtoId<TraitPrototype>>(_traitPreferences) { traitId };
|
||||
|
||||
if (traitCategory == null || traitCategory.MaxTraitPoints < 0)
|
||||
if (traitCategory.MaxPoints < 0) // DeltaV 13/01/26 - Traits: Changed to MaxPoints
|
||||
{
|
||||
return new(this)
|
||||
{
|
||||
|
|
@ -470,7 +471,7 @@ namespace Content.Shared.Preferences
|
|||
count += otherProto.Cost;
|
||||
}
|
||||
|
||||
if (count > traitCategory.MaxTraitPoints && traitProto.Cost != 0)
|
||||
if (count > traitCategory.MaxPoints && traitProto.Cost != 0) // DeltaV 13/01/26 - Traits: Changed to MaxPoints
|
||||
{
|
||||
return new(this);
|
||||
}
|
||||
|
|
@ -724,11 +725,11 @@ namespace Content.Shared.Preferences
|
|||
continue;
|
||||
|
||||
// Always valid.
|
||||
if (traitProto.Category == null)
|
||||
{
|
||||
result.Add(trait);
|
||||
continue;
|
||||
}
|
||||
// if (traitProto.Category == null) // DeltaV 13/01/26 - Traits rework
|
||||
// {
|
||||
// result.Add(trait);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// No category so dump it.
|
||||
if (!protoManager.Resolve(traitProto.Category, out var category))
|
||||
|
|
@ -738,7 +739,7 @@ namespace Content.Shared.Preferences
|
|||
existing += traitProto.Cost;
|
||||
|
||||
// Too expensive.
|
||||
if (existing > category.MaxTraitPoints)
|
||||
if (existing > category.MaxPoints) // DeltaV 13/01/26 - Traits: Was MaxTraitPoints
|
||||
continue;
|
||||
|
||||
groups[category.ID] = existing;
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Traits;
|
||||
|
||||
/// <summary>
|
||||
/// Traits category with general settings. Allows you to limit the number of taken traits in one category
|
||||
/// </summary>
|
||||
[Prototype]
|
||||
public sealed partial class TraitCategoryPrototype : IPrototype
|
||||
{
|
||||
public const string Default = "Default";
|
||||
|
||||
[ViewVariables]
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the trait category displayed in the UI
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId Name { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of traits that can be taken in this category.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int? MaxTraitPoints;
|
||||
}
|
||||
// using Robust.Shared.Prototypes; // DeltaV - Traits rework
|
||||
//
|
||||
// namespace Content.Shared.Traits;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Traits category with general settings. Allows you to limit the number of taken traits in one category
|
||||
// /// </summary>
|
||||
// [Prototype]
|
||||
// public sealed partial class TraitCategoryPrototype : IPrototype
|
||||
// {
|
||||
// public const string Default = "Default";
|
||||
//
|
||||
// [ViewVariables]
|
||||
// [IdDataField]
|
||||
// public string ID { get; private set; } = default!;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Name of the trait category displayed in the UI
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public LocId Name { get; private set; } = string.Empty;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// The maximum number of traits that can be taken in this category.
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public int? MaxTraitPoints;
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,76 +1,76 @@
|
|||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Humanoid.Prototypes; // DeltaV - Trait species hiding
|
||||
|
||||
namespace Content.Shared.Traits;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a trait.
|
||||
/// </summary>
|
||||
[Prototype]
|
||||
public sealed partial class TraitPrototype : IPrototype
|
||||
{
|
||||
[ViewVariables]
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The name of this trait.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId Name { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The description of this trait.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? Description { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Don't apply this trait to entities this whitelist IS NOT valid for.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// Don't apply this trait to entities this whitelist IS valid for. (hence, a blacklist)
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
/// <summary>
|
||||
/// The components that get added to the player, when they pick this trait.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ComponentRegistry Components { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// DeltaV - Components that get added to the player, overriding any existing instances of the component if they exist.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ComponentRegistry? OverriddenComponents { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gear that is given to the player, when they pick this trait.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId? TraitGear;
|
||||
|
||||
/// <summary>
|
||||
/// Trait Price. If negative number, points will be added.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Cost = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a trait to a category, allowing you to limit the selection of some traits to the settings of that category.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<TraitCategoryPrototype>? Category;
|
||||
|
||||
/// <summary>
|
||||
/// DeltaV - Hides traits from specific species
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<ProtoId<SpeciesPrototype>> ExcludedSpecies = new();
|
||||
}
|
||||
// using Content.Shared.Whitelist; // DeltaV - Traits rework
|
||||
// using Robust.Shared.Prototypes;
|
||||
// using Content.Shared.Humanoid.Prototypes; // DeltaV - Trait species hiding
|
||||
//
|
||||
// namespace Content.Shared.Traits;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Describes a trait.
|
||||
// /// </summary>
|
||||
// [Prototype]
|
||||
// public sealed partial class TraitPrototype : IPrototype
|
||||
// {
|
||||
// [ViewVariables]
|
||||
// [IdDataField]
|
||||
// public string ID { get; private set; } = default!;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// The name of this trait.
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public LocId Name { get; private set; } = string.Empty;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// The description of this trait.
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public LocId? Description { get; private set; }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Don't apply this trait to entities this whitelist IS NOT valid for.
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public EntityWhitelist? Whitelist;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Don't apply this trait to entities this whitelist IS valid for. (hence, a blacklist)
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public EntityWhitelist? Blacklist;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// The components that get added to the player, when they pick this trait.
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public ComponentRegistry Components { get; private set; } = default!;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// DeltaV - Components that get added to the player, overriding any existing instances of the component if they exist.
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public ComponentRegistry? OverriddenComponents { get; private set; }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Gear that is given to the player, when they pick this trait.
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public EntProtoId? TraitGear;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Trait Price. If negative number, points will be added.
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public int Cost = 0;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Adds a trait to a category, allowing you to limit the selection of some traits to the settings of that category.
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public ProtoId<TraitCategoryPrototype>? Category;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// DeltaV - Hides traits from specific species
|
||||
// /// </summary>
|
||||
// [DataField]
|
||||
// public HashSet<ProtoId<SpeciesPrototype>> ExcludedSpecies = new();
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -124,6 +124,23 @@ public sealed partial class DCCVars
|
|||
public static readonly CVarDef<int> YearOffset =
|
||||
CVarDef.Create("game.current_year_offset", 550, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* Traits
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of traits that can be selected globally.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> MaxTraitCount =
|
||||
CVarDef.Create("traits.max_count", 10, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum trait points available to spend.
|
||||
/// Traits with positive cost consume points, negative cost traits grant points.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> MaxTraitPoints =
|
||||
CVarDef.Create("traits.max_points", 15, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/*
|
||||
* Feedback webhook
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for trait conditions. Implementations check if a trait can be applied to a player.
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors, UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
|
||||
public abstract partial class BaseTraitCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, inverts the result of the condition.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Invert;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the condition, applying inversion if configured.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public bool Evaluate(TraitConditionContext ctx)
|
||||
{
|
||||
var result = EvaluateImplementation(ctx);
|
||||
return result ^ Invert;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a human-readable tooltip describing this condition's requirements.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public abstract string GetTooltip(IPrototypeManager proto, ILocalizationManager loc);
|
||||
|
||||
protected abstract bool EvaluateImplementation(TraitConditionContext ctx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context passed to trait conditions for evaluation.
|
||||
/// Contains references to the player and relevant systems.
|
||||
/// </summary>
|
||||
public sealed class TraitConditionContext
|
||||
{
|
||||
public required EntityUid Player { get; init; }
|
||||
public required ICommonSession? Session { get; init; }
|
||||
public required IEntityManager EntMan { get; init; }
|
||||
public required IPrototypeManager Proto { get; init; }
|
||||
public required IComponentFactory CompFactory { get; init; }
|
||||
public required ILogManager LogMan { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The job ID of the player, if available.
|
||||
/// </summary>
|
||||
public string? JobId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The species ID of the player, if available.
|
||||
/// </summary>
|
||||
public string? SpeciesId { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that checks if the player has a specific component.
|
||||
/// Use Invert = true to check if the player does NOT have the component.
|
||||
/// </summary>
|
||||
public sealed partial class HasCompCondition : BaseTraitCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The component name to check for (e.g., "Pacifism").
|
||||
/// </summary>
|
||||
[DataField(required: true, customTypeSerializer: typeof(ComponentNameSerializer))]
|
||||
public string Component = string.Empty;
|
||||
|
||||
protected override bool EvaluateImplementation(TraitConditionContext ctx)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Component))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var compType = ctx.CompFactory.GetRegistration(Component).Type;
|
||||
return ctx.EntMan.HasComponent(ctx.Player, compType);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Log the actual error instead of silently catching
|
||||
ctx.LogMan.GetSawmill("traits").Error($"Failed to get component registration for '{Component}'");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc)
|
||||
{
|
||||
// No tooltip for this condition since we're dealing with comps
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that checks if the player has a specific job.
|
||||
/// Use Invert = true to check if the player does NOT have the job.
|
||||
/// </summary>
|
||||
public sealed partial class HasJobCondition : BaseTraitCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The job prototype ID to check for.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<JobPrototype> Job = string.Empty;
|
||||
|
||||
protected override bool EvaluateImplementation(TraitConditionContext ctx)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ctx.JobId))
|
||||
return false;
|
||||
|
||||
return ctx.JobId == Job;
|
||||
}
|
||||
|
||||
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc)
|
||||
{
|
||||
var jobName = Job.Id;
|
||||
var jobColor = "#ffffff";
|
||||
|
||||
if (proto.TryIndex(Job, out var jobProto))
|
||||
{
|
||||
jobName = loc.GetString(jobProto.Name);
|
||||
|
||||
// Try to find the job's department color
|
||||
foreach (var dept in proto.EnumeratePrototypes<DepartmentPrototype>())
|
||||
{
|
||||
if (dept.Roles.Contains(Job))
|
||||
{
|
||||
jobColor = dept.Color.ToHex();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Invert
|
||||
? loc.GetString("trait-condition-job-not", ("job", jobName), ("color", jobColor))
|
||||
: loc.GetString("trait-condition-job-is", ("job", jobName), ("color", jobColor));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that checks if the player's job is in a specific department.
|
||||
/// Use Invert = true to check if the player is NOT in the department.
|
||||
/// </summary>
|
||||
public sealed partial class InDepartmentCondition : BaseTraitCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The department prototype ID to check for.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<DepartmentPrototype> Department = string.Empty;
|
||||
|
||||
protected override bool EvaluateImplementation(TraitConditionContext ctx)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ctx.JobId))
|
||||
return false;
|
||||
|
||||
if (!ctx.Proto.TryIndex(Department, out var department))
|
||||
return false;
|
||||
|
||||
return department.Roles.Contains(ctx.JobId);
|
||||
}
|
||||
|
||||
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc)
|
||||
{
|
||||
var deptName = Department.Id;
|
||||
var deptColor = "#ffffff";
|
||||
|
||||
if (proto.TryIndex(Department, out var deptProto))
|
||||
{
|
||||
deptName = loc.GetString($"department-{deptProto.ID}");
|
||||
deptColor = deptProto.Color.ToHex();
|
||||
}
|
||||
|
||||
|
||||
return Invert
|
||||
? loc.GetString("trait-condition-department-not", ("department", deptName), ("color", deptColor))
|
||||
: loc.GetString("trait-condition-department-is", ("department", deptName), ("color", deptColor));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that checks if the player is a specific species.
|
||||
/// Use Invert = true to check if the player is NOT the species.
|
||||
/// </summary>
|
||||
public sealed partial class IsSpeciesCondition : BaseTraitCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The species ID to check for.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<SpeciesPrototype> Species = string.Empty;
|
||||
|
||||
protected override bool EvaluateImplementation(TraitConditionContext ctx)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ctx.SpeciesId))
|
||||
return false;
|
||||
|
||||
return ctx.SpeciesId == Species;
|
||||
}
|
||||
|
||||
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc)
|
||||
{
|
||||
var speciesName = Species.Id;
|
||||
if (proto.TryIndex(Species, out var speciesProto))
|
||||
{
|
||||
speciesName = loc.GetString(speciesProto.Name);
|
||||
}
|
||||
|
||||
return Invert
|
||||
? loc.GetString("trait-condition-species-not", ("species", speciesName))
|
||||
: loc.GetString("trait-condition-species-is", ("species", speciesName));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Effect that adds components to the player entity.
|
||||
/// Components are added without overwriting existing ones.
|
||||
/// </summary>
|
||||
public sealed partial class AddCompsEffect : BaseTraitEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The components to add to the entity.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ComponentRegistry Components = new();
|
||||
|
||||
public override void Apply(TraitEffectContext ctx)
|
||||
{
|
||||
ctx.EntMan.AddComponents(ctx.Player, Components, removeExisting: false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for trait effects. Implementations apply modifications to an entity when a trait is selected.
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors, UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
|
||||
public abstract partial class BaseTraitEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the effect to the target entity.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public abstract void Apply(TraitEffectContext ctx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context passed to trait effects for application.
|
||||
/// Contains references to the player entity and relevant systems.
|
||||
/// </summary>
|
||||
public sealed class TraitEffectContext
|
||||
{
|
||||
public required EntityUid Player { get; init; }
|
||||
public required IEntityManager EntMan { get; init; }
|
||||
public required IPrototypeManager Proto { get; init; }
|
||||
public required IComponentFactory CompFactory { get; init; }
|
||||
public required ILogManager LogMan { get; init; }
|
||||
public required TransformComponent Transform { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Effect that overrides component fields on the player entity.
|
||||
/// If the component exists, its fields are overwritten with the new values.
|
||||
/// If it doesn't exist, the component is added.
|
||||
/// </summary>
|
||||
public sealed partial class OverrideCompsEffect : BaseTraitEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The components to add/override on the entity.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ComponentRegistry Components = new();
|
||||
|
||||
public override void Apply(TraitEffectContext ctx)
|
||||
{
|
||||
ctx.EntMan.AddComponents(ctx.Player, Components, removeExisting: true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Effect that removes components from the player entity if they exist.
|
||||
/// </summary>
|
||||
public sealed partial class RemCompsEffect : BaseTraitEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The component names to remove from the entity.
|
||||
/// </summary>
|
||||
[DataField(required: true, customTypeSerializer: typeof(CustomHashSetSerializer<string, ComponentNameSerializer>))]
|
||||
public HashSet<string> Components = new();
|
||||
|
||||
public override void Apply(TraitEffectContext ctx)
|
||||
{
|
||||
foreach (var compName in Components)
|
||||
{
|
||||
if (!ctx.CompFactory.TryGetRegistration(compName, out var registration))
|
||||
{
|
||||
var sawmill = ctx.LogMan.GetSawmill("traits");
|
||||
sawmill.Warning($"RemCompsEffect references unknown component: {compName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx.EntMan.RemoveComponent(ctx.Player, registration.Type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Effect that spawns an item and attempts to place it in the player's hand.
|
||||
/// If the player cannot hold the item, it is spawned at their feet.
|
||||
/// </summary>
|
||||
public sealed partial class SpawnItemInHandEffect : BaseTraitEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity prototype to spawn.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public EntProtoId Item = string.Empty;
|
||||
|
||||
public override void Apply(TraitEffectContext ctx)
|
||||
{
|
||||
// This effect needs to be applied server-side where we have access to
|
||||
// SharedHandsSystem. The actual spawning logic is handled by the server TraitSystem.
|
||||
// This class just holds the data.
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits;
|
||||
|
||||
/// <summary>
|
||||
/// Prototype for a category of traits.
|
||||
/// Categories organize traits and can impose their own limits.
|
||||
/// </summary>
|
||||
[Prototype]
|
||||
public sealed partial class TraitCategoryPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Localization key for the category's display name.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public LocId Name;
|
||||
|
||||
/// <summary>
|
||||
/// Display order priority. Lower values appear first.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Priority;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of traits that can be selected from this category.
|
||||
/// Null means unlimited (only global limit applies).
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int? MaxTraits;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum trait points that can be spent in this category.
|
||||
/// Null means unlimited (only global limit applies).
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int? MaxPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Color hex for the category header accent.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Color AccentColor = Color.FromHex("#4a9eff");
|
||||
|
||||
/// <summary>
|
||||
/// Whether this category starts expanded or collapsed.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool DefaultExpanded = true;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using Content.Shared._DV.Traits.Conditions;
|
||||
using Content.Shared._DV.Traits.Effects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Traits;
|
||||
|
||||
/// <summary>
|
||||
/// Prototype for a character trait in DeltaV.
|
||||
/// Traits modify character behavior through condition-checked effects.
|
||||
/// </summary>
|
||||
[Prototype]
|
||||
public sealed partial class TraitPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Localization key for the trait's display name.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public LocId Name;
|
||||
|
||||
/// <summary>
|
||||
/// Localization key for the trait's description.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public LocId Description;
|
||||
|
||||
/// <summary>
|
||||
/// The category this trait belongs to.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<TraitCategoryPrototype> Category;
|
||||
|
||||
/// <summary>
|
||||
/// How many trait points this trait costs (positive) or grants (negative).
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Cost = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Conditions that must be met for this trait to be selectable and applied.
|
||||
/// All conditions must pass for the trait to be valid.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<BaseTraitCondition> Conditions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Effects to apply to the entity when this trait is selected.
|
||||
/// Effects are applied in order.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<BaseTraitEffect> Effects = new();
|
||||
|
||||
/// <summary>
|
||||
/// Other traits that are mutually exclusive with this one.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<TraitPrototype>> Conflicts = new();
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
trait-category-disabilities = Disabilities
|
||||
trait-category-medical = Medical
|
||||
trait-category-mental = Mental
|
||||
trait-category-accents = Accents
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
## Traits Editor UI
|
||||
trait-editor-title = Character Traits
|
||||
trait-editor-points-label = Available Points
|
||||
trait-editor-search-placeholder = Search traits...
|
||||
trait-editor-footer-hint = Hover over traits for details
|
||||
trait-editor-footer-info = Negative costs grant bonus points
|
||||
|
||||
## Category suffixes
|
||||
trait-category-traits = {$selected} / {$max} traits
|
||||
trait-category-traits-unlimited = {$selected} traits
|
||||
trait-category-points = ({$selected} / {$max} pts)
|
||||
|
||||
## Condition tooltips
|
||||
trait-conditions-tooltip = [bold]Requirements:[/bold]
|
||||
{$requirements}
|
||||
trait-conditions-not-met-tooltip = Requirements not met:
|
||||
{$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].
|
||||
|
||||
## Job conditions
|
||||
trait-condition-job-is = You must be a [color={$color}]{$job}[/color].
|
||||
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.
|
||||
|
|
@ -59,6 +59,7 @@ humanoid-profile-editor-no-traits = No traits available
|
|||
|
||||
humanoid-profile-editor-trait-count-hint = Points available: [{$current}/{$max}]
|
||||
|
||||
trait-category-disabilities = Disabilities
|
||||
# DeltaV - Custom traits; Moved to _DV to be next to the description
|
||||
#trait-category-disabilities = Disabilities
|
||||
trait-category-speech = Speech traits
|
||||
trait-category-quirks = Quirks
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
- type: traitCategory
|
||||
id: Disabilities
|
||||
name: trait-category-disabilities
|
||||
# DELTA-V
|
||||
# Due to the custom trait system, all traits have been moved under \Resources\Prototypes\_DV\Traits
|
||||
# If you want to add a new one, do it there
|
||||
|
||||
- type: traitCategory
|
||||
id: SpeechTraits
|
||||
name: trait-category-speech
|
||||
maxTraitPoints: 2
|
||||
|
||||
- type: traitCategory
|
||||
id: Quirks
|
||||
name: trait-category-quirks
|
||||
#- type: traitCategory
|
||||
# id: Disabilities
|
||||
# name: trait-category-disabilities
|
||||
#
|
||||
#- type: traitCategory
|
||||
# id: SpeechTraits
|
||||
# name: trait-category-speech
|
||||
# maxTraitPoints: 2
|
||||
#
|
||||
#- type: traitCategory
|
||||
# id: Quirks
|
||||
# name: trait-category-quirks
|
||||
|
|
|
|||
|
|
@ -1,104 +1,108 @@
|
|||
# DELTA-V
|
||||
# Due to the custom trait system, all traits have been moved under \Resources\Prototypes\_DV\Traits
|
||||
# If you want to add a new one, do it there
|
||||
|
||||
# If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly!
|
||||
|
||||
- type: trait
|
||||
id: Blindness
|
||||
name: trait-blindness-name
|
||||
description: trait-blindness-desc
|
||||
traitGear: WhiteCane
|
||||
category: Disabilities
|
||||
whitelist:
|
||||
components:
|
||||
- Blindable
|
||||
components:
|
||||
- type: PermanentBlindness
|
||||
|
||||
- type: trait
|
||||
id: PoorVision
|
||||
name: trait-poor-vision-name
|
||||
description: trait-poor-vision-desc
|
||||
traitGear: ClothingEyesGlasses
|
||||
category: Disabilities
|
||||
whitelist:
|
||||
components:
|
||||
- Blindable
|
||||
components:
|
||||
- type: PermanentBlindness
|
||||
blindness: 4
|
||||
|
||||
- type: trait
|
||||
id: Narcolepsy
|
||||
name: trait-narcolepsy-name
|
||||
description: trait-narcolepsy-desc
|
||||
category: Disabilities
|
||||
components:
|
||||
- type: Narcolepsy
|
||||
maxTimeBetweenIncidents: 600
|
||||
minTimeBetweenIncidents: 300
|
||||
maxDurationOfIncident: 30
|
||||
minDurationOfIncident: 10
|
||||
|
||||
- type: trait
|
||||
id: Unrevivable
|
||||
name: trait-unrevivable-name
|
||||
description: trait-unrevivable-desc
|
||||
category: Disabilities
|
||||
components:
|
||||
- type: Unrevivable
|
||||
cloneable: true
|
||||
|
||||
- type: trait
|
||||
id: Monochromacy
|
||||
name: trait-monochromacy-name
|
||||
description: trait-monochromacy-desc
|
||||
category: Disabilities
|
||||
components:
|
||||
- type: BlackAndWhiteOverlay
|
||||
|
||||
- type: trait
|
||||
id: Muted
|
||||
name: trait-muted-name
|
||||
description: trait-muted-desc
|
||||
category: Disabilities
|
||||
blacklist:
|
||||
components:
|
||||
- BorgChassis
|
||||
components:
|
||||
- type: Muted
|
||||
|
||||
- type: trait
|
||||
id: Paracusia
|
||||
name: trait-paracusia-name
|
||||
description: trait-paracusia-desc
|
||||
category: Disabilities
|
||||
components:
|
||||
- type: Paracusia
|
||||
minTimeBetweenIncidents: 0.1
|
||||
maxTimeBetweenIncidents: 300
|
||||
maxSoundDistance: 7
|
||||
sounds:
|
||||
collection: Paracusia
|
||||
|
||||
- type: trait
|
||||
id: PainNumbness
|
||||
name: trait-painnumbness-name
|
||||
description: trait-painnumbness-desc
|
||||
category: Disabilities
|
||||
components:
|
||||
- type: PainNumbness
|
||||
|
||||
- type: trait
|
||||
id: ImpairedMobility
|
||||
name: trait-impaired-mobility-name
|
||||
description: trait-impaired-mobility-desc
|
||||
traitGear: OffsetCane
|
||||
category: Disabilities
|
||||
components:
|
||||
- type: ImpairedMobility
|
||||
|
||||
- type: trait
|
||||
id: Hemophilia
|
||||
name: trait-hemophilia-name
|
||||
description: trait-hemophilia-desc
|
||||
category: Disabilities
|
||||
components:
|
||||
- type: Hemophilia
|
||||
#
|
||||
#- type: trait
|
||||
# id: Blindness
|
||||
# name: trait-blindness-name
|
||||
# description: trait-blindness-desc
|
||||
# traitGear: WhiteCane
|
||||
# category: Disabilities
|
||||
# whitelist:
|
||||
# components:
|
||||
# - Blindable
|
||||
# components:
|
||||
# - type: PermanentBlindness
|
||||
#
|
||||
#- type: trait
|
||||
# id: PoorVision
|
||||
# name: trait-poor-vision-name
|
||||
# description: trait-poor-vision-desc
|
||||
# traitGear: ClothingEyesGlasses
|
||||
# category: Disabilities
|
||||
# whitelist:
|
||||
# components:
|
||||
# - Blindable
|
||||
# components:
|
||||
# - type: PermanentBlindness
|
||||
# blindness: 4
|
||||
#
|
||||
#- type: trait
|
||||
# id: Narcolepsy
|
||||
# name: trait-narcolepsy-name
|
||||
# description: trait-narcolepsy-desc
|
||||
# category: Disabilities
|
||||
# components:
|
||||
# - type: Narcolepsy
|
||||
# maxTimeBetweenIncidents: 600
|
||||
# minTimeBetweenIncidents: 300
|
||||
# maxDurationOfIncident: 30
|
||||
# minDurationOfIncident: 10
|
||||
#
|
||||
#- type: trait
|
||||
# id: Unrevivable
|
||||
# name: trait-unrevivable-name
|
||||
# description: trait-unrevivable-desc
|
||||
# category: Disabilities
|
||||
# components:
|
||||
# - type: Unrevivable
|
||||
# cloneable: true
|
||||
#
|
||||
#- type: trait
|
||||
# id: Monochromacy
|
||||
# name: trait-monochromacy-name
|
||||
# description: trait-monochromacy-desc
|
||||
# category: Disabilities
|
||||
# components:
|
||||
# - type: BlackAndWhiteOverlay
|
||||
#
|
||||
#- type: trait
|
||||
# id: Muted
|
||||
# name: trait-muted-name
|
||||
# description: trait-muted-desc
|
||||
# category: Disabilities
|
||||
# blacklist:
|
||||
# components:
|
||||
# - BorgChassis
|
||||
# components:
|
||||
# - type: Muted
|
||||
#
|
||||
#- type: trait
|
||||
# id: Paracusia
|
||||
# name: trait-paracusia-name
|
||||
# description: trait-paracusia-desc
|
||||
# category: Disabilities
|
||||
# components:
|
||||
# - type: Paracusia
|
||||
# minTimeBetweenIncidents: 0.1
|
||||
# maxTimeBetweenIncidents: 300
|
||||
# maxSoundDistance: 7
|
||||
# sounds:
|
||||
# collection: Paracusia
|
||||
#
|
||||
#- type: trait
|
||||
# id: PainNumbness
|
||||
# name: trait-painnumbness-name
|
||||
# description: trait-painnumbness-desc
|
||||
# category: Disabilities
|
||||
# components:
|
||||
# - type: PainNumbness
|
||||
#
|
||||
#- type: trait
|
||||
# id: ImpairedMobility
|
||||
# name: trait-impaired-mobility-name
|
||||
# description: trait-impaired-mobility-desc
|
||||
# traitGear: OffsetCane
|
||||
# category: Disabilities
|
||||
# components:
|
||||
# - type: ImpairedMobility
|
||||
#
|
||||
#- type: trait
|
||||
# id: Hemophilia
|
||||
# name: trait-hemophilia-name
|
||||
# description: trait-hemophilia-desc
|
||||
# category: Disabilities
|
||||
# components:
|
||||
# - type: Hemophilia
|
||||
|
|
|
|||
|
|
@ -1,26 +1,30 @@
|
|||
# If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly!
|
||||
# DELTA-V
|
||||
# Due to the custom trait system, all traits have been moved under \Resources\Prototypes\_DV\Traits
|
||||
# If you want to add a new one, do it there
|
||||
|
||||
- type: trait
|
||||
id: Pacifist
|
||||
name: trait-pacifist-name
|
||||
description: trait-pacifist-desc
|
||||
category: Quirks
|
||||
components:
|
||||
- type: Pacified
|
||||
|
||||
- type: trait
|
||||
id: LightweightDrunk
|
||||
name: trait-lightweight-name
|
||||
description: trait-lightweight-desc
|
||||
category: Quirks
|
||||
components:
|
||||
- type: LightweightDrunk
|
||||
boozeStrengthMultiplier: 2
|
||||
|
||||
- type: trait
|
||||
id: Snoring
|
||||
name: trait-snoring-name
|
||||
description: trait-snoring-desc
|
||||
category: Quirks
|
||||
components:
|
||||
- type: Snoring
|
||||
## If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly!
|
||||
#
|
||||
#- type: trait
|
||||
# id: Pacifist
|
||||
# name: trait-pacifist-name
|
||||
# description: trait-pacifist-desc
|
||||
# category: Quirks
|
||||
# components:
|
||||
# - type: Pacified
|
||||
#
|
||||
#- type: trait
|
||||
# id: LightweightDrunk
|
||||
# name: trait-lightweight-name
|
||||
# description: trait-lightweight-desc
|
||||
# category: Quirks
|
||||
# components:
|
||||
# - type: LightweightDrunk
|
||||
# boozeStrengthMultiplier: 2
|
||||
#
|
||||
#- type: trait
|
||||
# id: Snoring
|
||||
# name: trait-snoring-name
|
||||
# description: trait-snoring-desc
|
||||
# category: Quirks
|
||||
# components:
|
||||
# - type: Snoring
|
||||
|
|
|
|||
|
|
@ -1,91 +1,95 @@
|
|||
# If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly!
|
||||
|
||||
# Free
|
||||
|
||||
- type: trait
|
||||
id: Accentless
|
||||
name: trait-accentless-name
|
||||
description: trait-accentless-desc
|
||||
category: SpeechTraits
|
||||
cost: 0 # Delta-V change- removed cost for accentless to 0, as it said it was supposed to be free
|
||||
components:
|
||||
- type: Accentless
|
||||
removes:
|
||||
- type: LizardAccent
|
||||
- type: MothAccent
|
||||
- type: ReplacementAccent
|
||||
accent: dwarf
|
||||
|
||||
- type: trait
|
||||
id: Liar
|
||||
name: trait-liar-name
|
||||
description: trait-liar-desc
|
||||
category: SpeechTraits
|
||||
cost: 0 # Delta-V change- removed cost for liar to 0, as was discussed
|
||||
components:
|
||||
- type: ReplacementAccent
|
||||
accent: liar
|
||||
|
||||
- type: trait
|
||||
id: SocialAnxiety
|
||||
name: trait-socialanxiety-name
|
||||
description: trait-socialanxiety-desc
|
||||
category: SpeechTraits
|
||||
cost: 0 # Delta-V change- lowered cost to 0 to allow for more versatility with accents
|
||||
components:
|
||||
- type: StutteringAccent
|
||||
matchRandomProb: 0.1
|
||||
fourRandomProb: 0
|
||||
threeRandomProb: 0
|
||||
cutRandomProb: 0
|
||||
|
||||
- type: trait
|
||||
id: FrontalLisp
|
||||
name: trait-frontal-lisp-name
|
||||
description: trait-frontal-lisp-desc
|
||||
category: SpeechTraits
|
||||
cost: 0 # Delta-V change- lowered cost to 0 to allow for more versatility with accents
|
||||
components:
|
||||
- type: FrontalLisp
|
||||
|
||||
# 2 Cost
|
||||
|
||||
- type: trait
|
||||
id: SouthernAccent
|
||||
name: trait-southern-name
|
||||
description: trait-southern-desc
|
||||
category: SpeechTraits
|
||||
cost: 2 # Delta-V change- weighted up accents to 2 as discussed for accent stacking being off
|
||||
components:
|
||||
- type: SouthernAccent
|
||||
|
||||
- type: trait
|
||||
id: PirateAccent
|
||||
name: trait-pirate-accent-name
|
||||
description: trait-pirate-accent-desc
|
||||
category: SpeechTraits
|
||||
cost: 2 # Delta-V change- weighted up accents to 2 as discussed for accent stacking being off
|
||||
components:
|
||||
- type: PirateAccent
|
||||
|
||||
- type: trait
|
||||
id: CowboyAccent
|
||||
name: trait-cowboy-name
|
||||
description: trait-cowboy-desc
|
||||
category: SpeechTraits
|
||||
cost: 2 # Delta-V change- weighted up accents to 2 as discussed for accent stacking being off
|
||||
components:
|
||||
- type: ReplacementAccent
|
||||
accent: cowboy
|
||||
|
||||
- type: trait
|
||||
id: ItalianAccent
|
||||
name: trait-italian-name
|
||||
description: trait-italian-desc
|
||||
category: SpeechTraits
|
||||
cost: 2 # Delta-V change- weighted up accents to 2 as discussed for accent stacking being off
|
||||
components:
|
||||
- type: ReplacementAccent
|
||||
accent: italian
|
||||
|
||||
# DELTA-V
|
||||
# Due to the custom trait system, all traits have been moved under \Resources\Prototypes\_DV\Traits
|
||||
# If you want to add a new one, do it there
|
||||
|
||||
## If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly!
|
||||
#
|
||||
## Free
|
||||
#
|
||||
#- type: trait
|
||||
# id: Accentless
|
||||
# name: trait-accentless-name
|
||||
# description: trait-accentless-desc
|
||||
# category: SpeechTraits
|
||||
# cost: 0 # Delta-V change- removed cost for accentless to 0, as it said it was supposed to be free
|
||||
# components:
|
||||
# - type: Accentless
|
||||
# removes:
|
||||
# - type: LizardAccent
|
||||
# - type: MothAccent
|
||||
# - type: ReplacementAccent
|
||||
# accent: dwarf
|
||||
#
|
||||
#- type: trait
|
||||
# id: Liar
|
||||
# name: trait-liar-name
|
||||
# description: trait-liar-desc
|
||||
# category: SpeechTraits
|
||||
# cost: 0 # Delta-V change- removed cost for liar to 0, as was discussed
|
||||
# components:
|
||||
# - type: ReplacementAccent
|
||||
# accent: liar
|
||||
#
|
||||
#- type: trait
|
||||
# id: SocialAnxiety
|
||||
# name: trait-socialanxiety-name
|
||||
# description: trait-socialanxiety-desc
|
||||
# category: SpeechTraits
|
||||
# cost: 0 # Delta-V change- lowered cost to 0 to allow for more versatility with accents
|
||||
# components:
|
||||
# - type: StutteringAccent
|
||||
# matchRandomProb: 0.1
|
||||
# fourRandomProb: 0
|
||||
# threeRandomProb: 0
|
||||
# cutRandomProb: 0
|
||||
#
|
||||
#- type: trait
|
||||
# id: FrontalLisp
|
||||
# name: trait-frontal-lisp-name
|
||||
# description: trait-frontal-lisp-desc
|
||||
# category: SpeechTraits
|
||||
# cost: 0 # Delta-V change- lowered cost to 0 to allow for more versatility with accents
|
||||
# components:
|
||||
# - type: FrontalLisp
|
||||
#
|
||||
## 2 Cost
|
||||
#
|
||||
#- type: trait
|
||||
# id: SouthernAccent
|
||||
# name: trait-southern-name
|
||||
# description: trait-southern-desc
|
||||
# category: SpeechTraits
|
||||
# cost: 2 # Delta-V change- weighted up accents to 2 as discussed for accent stacking being off
|
||||
# components:
|
||||
# - type: SouthernAccent
|
||||
#
|
||||
#- type: trait
|
||||
# id: PirateAccent
|
||||
# name: trait-pirate-accent-name
|
||||
# description: trait-pirate-accent-desc
|
||||
# category: SpeechTraits
|
||||
# cost: 2 # Delta-V change- weighted up accents to 2 as discussed for accent stacking being off
|
||||
# components:
|
||||
# - type: PirateAccent
|
||||
#
|
||||
#- type: trait
|
||||
# id: CowboyAccent
|
||||
# name: trait-cowboy-name
|
||||
# description: trait-cowboy-desc
|
||||
# category: SpeechTraits
|
||||
# cost: 2 # Delta-V change- weighted up accents to 2 as discussed for accent stacking being off
|
||||
# components:
|
||||
# - type: ReplacementAccent
|
||||
# accent: cowboy
|
||||
#
|
||||
#- type: trait
|
||||
# id: ItalianAccent
|
||||
# name: trait-italian-name
|
||||
# description: trait-italian-desc
|
||||
# category: SpeechTraits
|
||||
# cost: 2 # Delta-V change- weighted up accents to 2 as discussed for accent stacking being off
|
||||
# components:
|
||||
# - type: ReplacementAccent
|
||||
# accent: italian
|
||||
#
|
||||
#
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
- type: trait
|
||||
category: Quirks
|
||||
id: Synthetic
|
||||
name: trait-synth-name
|
||||
description: trait-synth-desc
|
||||
# Begin DeltaV Additions - blacklist IPCs
|
||||
blacklist:
|
||||
components:
|
||||
- Silicon
|
||||
- BorgChassis
|
||||
excludedSpecies:
|
||||
- IPC
|
||||
# End DeltaV Additions - blacklist IPCs
|
||||
components:
|
||||
- type: Synth
|
||||
# DELTA-V
|
||||
# Due to the custom trait system, all traits have been moved under \Resources\Prototypes\_DV\Traits
|
||||
# If you want to add a new one, do it there
|
||||
|
||||
#- type: trait
|
||||
# category: Quirks
|
||||
# id: Synthetic
|
||||
# name: trait-synth-name
|
||||
# description: trait-synth-desc
|
||||
# # Begin DeltaV Additions - blacklist IPCs
|
||||
# blacklist:
|
||||
# components:
|
||||
# - Silicon
|
||||
# - BorgChassis
|
||||
# excludedSpecies:
|
||||
# - IPC
|
||||
# # End DeltaV Additions - blacklist IPCs
|
||||
# components:
|
||||
# - type: Synth
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
- type: trait
|
||||
id: UltraVision
|
||||
name: trait-ultravision-name
|
||||
description: trait-ultravision-desc
|
||||
category: Disabilities
|
||||
components:
|
||||
- type: UltraVision
|
||||
|
||||
- type: trait
|
||||
id: DogVision
|
||||
name: trait-deuteranopia-name
|
||||
description: trait-deuteranopia-desc
|
||||
category: Disabilities
|
||||
components:
|
||||
- type: DogVision
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
- type: trait
|
||||
id: ArmAmputeeL
|
||||
name: trait-amputee-left-arm-name
|
||||
description: trait-amputee-left-arm-desc
|
||||
category: Amputee
|
||||
cost: 1
|
||||
components:
|
||||
- type: Amputee
|
||||
removeBodyPart: Arm
|
||||
partSymmetry: Left
|
||||
|
||||
- type: trait
|
||||
id: ArmAmputeeR
|
||||
name: trait-amputee-right-arm-name
|
||||
description: trait-amputee-right-arm-desc
|
||||
category: Amputee
|
||||
cost: 1
|
||||
components:
|
||||
- type: Amputee
|
||||
removeBodyPart: Arm
|
||||
partSymmetry: Right
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
- type: traitCategory
|
||||
id: Amputee
|
||||
name: trait-category-amputee
|
||||
maxTraitPoints: 1
|
||||
|
|
@ -1,54 +1,132 @@
|
|||
- type: trait
|
||||
id: Addicted
|
||||
name: trait-addicted-name
|
||||
description: trait-addicted-desc
|
||||
category: Disabilities
|
||||
components:
|
||||
- type: Addicted
|
||||
## If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly!
|
||||
|
||||
- type: trait
|
||||
id: InPain
|
||||
name: trait-inpain-name
|
||||
description: trait-inpain-desc
|
||||
traitGear: PillCanisterSoretizone
|
||||
id: Blindness
|
||||
name: trait-blindness-name
|
||||
description: trait-blindness-desc
|
||||
category: Disabilities
|
||||
conditions:
|
||||
- !type:HasCompCondition
|
||||
component: Blindable
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Pain
|
||||
- type: PermanentBlindness
|
||||
- !type:SpawnItemInHandEffect
|
||||
item: WhiteCane
|
||||
|
||||
- type: trait
|
||||
id: Unborgable
|
||||
name: trait-unborgable-name
|
||||
description: trait-unborgable-desc
|
||||
id: PoorVision
|
||||
name: trait-poor-vision-name
|
||||
description: trait-poor-vision-desc
|
||||
category: Disabilities
|
||||
conditions:
|
||||
- !type:HasCompCondition
|
||||
component: Blindable
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Unborgable # Automatically gets moved to the brain
|
||||
- type: PermanentBlindness
|
||||
blindness: 4
|
||||
- !type:SpawnItemInHandEffect
|
||||
item: ClothingEyesGlasses
|
||||
|
||||
- type: trait
|
||||
id: Depression
|
||||
name: trait-depression-name
|
||||
description: trait-depression-desc
|
||||
traitGear: PillCanisterNeurozenium
|
||||
id: Monochromacy
|
||||
name: trait-monochromacy-name
|
||||
description: trait-monochromacy-desc
|
||||
category: Disabilities
|
||||
components: []
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: BlackAndWhiteOverlay
|
||||
|
||||
- type: trait
|
||||
id: Deuteranopia
|
||||
name: trait-deuteranopia-name
|
||||
description: trait-deuteranopia-desc
|
||||
category: Disabilities
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: DogVision
|
||||
|
||||
- type: trait
|
||||
id: UltraVision
|
||||
name: trait-ultravision-name
|
||||
description: trait-ultravision-desc
|
||||
category: Disabilities
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: UltraVision
|
||||
|
||||
- type: trait
|
||||
id: ImpairedMobility
|
||||
name: trait-impaired-mobility-name
|
||||
description: trait-impaired-mobility-desc
|
||||
category: Disabilities
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: ImpairedMobility
|
||||
- !type:SpawnItemInHandEffect
|
||||
item: OffsetCane
|
||||
|
||||
- type: trait
|
||||
id: ArmAmputeeLeft
|
||||
name: trait-amputee-left-arm-name
|
||||
description: trait-amputee-left-arm-desc
|
||||
category: Disabilities
|
||||
conflicts:
|
||||
- ArmAmputeeRight # TODO: AmputeeComponent doesn't stack, should be an effect instead
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Amputee
|
||||
removeBodyPart: Arm
|
||||
partSymmetry: Left
|
||||
|
||||
- type: trait
|
||||
id: ArmAmputeeRight
|
||||
name: trait-amputee-right-arm-name
|
||||
description: trait-amputee-right-arm-desc
|
||||
category: Disabilities
|
||||
conflicts:
|
||||
- ArmAmputeeLeft
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Amputee
|
||||
removeBodyPart: Arm
|
||||
partSymmetry: Right
|
||||
|
||||
- type: trait
|
||||
id: Dysgraphia
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-desc
|
||||
category: Disabilities
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: BlockWriting
|
||||
|
||||
- type: trait
|
||||
id: Redshirt
|
||||
name: trait-redshirt-name
|
||||
description: trait-redshirt-desc
|
||||
id: PainNumbness
|
||||
name: trait-painnumbness-name
|
||||
description: trait-painnumbness-desc
|
||||
category: Disabilities
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Redshirt
|
||||
overriddenComponents:
|
||||
- type: MobThresholds
|
||||
thresholds:
|
||||
0: Alive
|
||||
99.9: Critical
|
||||
100: Dead
|
||||
- type: PainNumbness
|
||||
|
||||
- type: trait
|
||||
id: Hemophilia
|
||||
name: trait-hemophilia-name
|
||||
description: trait-hemophilia-desc
|
||||
category: Disabilities
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Hemophilia
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
## If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly!
|
||||
|
||||
- type: trait
|
||||
id: Narcolepsy
|
||||
name: trait-narcolepsy-name
|
||||
description: trait-narcolepsy-desc
|
||||
category: Medical
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Narcolepsy
|
||||
maxTimeBetweenIncidents: 600
|
||||
minTimeBetweenIncidents: 300
|
||||
maxDurationOfIncident: 30
|
||||
minDurationOfIncident: 10
|
||||
|
||||
- type: trait
|
||||
id: InPain
|
||||
name: trait-inpain-name
|
||||
description: trait-inpain-desc
|
||||
category: Medical
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Pain
|
||||
- !type:SpawnItemInHandEffect
|
||||
item: PillCanisterSoretizone
|
||||
|
||||
- type: trait
|
||||
id: LightweightDrunk
|
||||
name: trait-lightweight-name
|
||||
description: trait-lightweight-desc
|
||||
category: Medical
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: LightweightDrunk
|
||||
boozeStrengthMultiplier: 2
|
||||
|
||||
- type: trait
|
||||
id: Unrevivable
|
||||
name: trait-unrevivable-name
|
||||
description: trait-unrevivable-desc
|
||||
category: Medical
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Unrevivable
|
||||
cloneable: true
|
||||
|
||||
- type: trait
|
||||
id: Uncloneable
|
||||
name: trait-uncloneable-name
|
||||
description: trait-uncloneable-desc
|
||||
category: Medical
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Unrevivable
|
||||
cloneable: false
|
||||
|
||||
- type: trait
|
||||
id: Unborgable
|
||||
name: trait-unborgable-name
|
||||
description: trait-unborgable-desc
|
||||
category: Medical
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Unborgable
|
||||
|
||||
- type: trait
|
||||
id: Redshirt
|
||||
name: trait-redshirt-name
|
||||
description: trait-redshirt-desc
|
||||
category: Medical
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Redshirt
|
||||
- !type:OverrideCompsEffect
|
||||
components:
|
||||
- type: MobThresholds
|
||||
thresholds:
|
||||
0: Alive
|
||||
99.9: Critical
|
||||
100: Dead
|
||||
|
||||
- type: trait
|
||||
id: Synthetic
|
||||
name: trait-synth-name
|
||||
description: trait-synth-desc
|
||||
category: Medical
|
||||
conditions:
|
||||
- !type:HasCompCondition
|
||||
component: Silicon
|
||||
invert: true
|
||||
- !type:HasCompCondition
|
||||
component: BorgChassis
|
||||
invert: true
|
||||
- !type:IsSpeciesCondition
|
||||
species: IPC
|
||||
invert: true
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Synth
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
## If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly!
|
||||
|
||||
- type: trait
|
||||
id: Paracusia
|
||||
name: trait-paracusia-name
|
||||
description: trait-paracusia-desc
|
||||
category: Mental
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Paracusia
|
||||
minTimeBetweenIncidents: 0.1
|
||||
maxTimeBetweenIncidents: 300
|
||||
maxSoundDistance: 7
|
||||
sounds:
|
||||
collection: Paracusia
|
||||
|
||||
- type: trait
|
||||
id: Depression
|
||||
name: trait-depression-name
|
||||
description: trait-depression-desc
|
||||
category: Mental
|
||||
effects:
|
||||
- !type:SpawnItemInHandEffect
|
||||
item: PillCanisterNeurozenium
|
||||
|
||||
- type: trait
|
||||
id: Addicted
|
||||
name: trait-addicted-name
|
||||
description: trait-addicted-desc
|
||||
category: Mental
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Addicted
|
||||
|
||||
- type: trait
|
||||
id: Liar
|
||||
name: trait-liar-name
|
||||
description: trait-liar-desc
|
||||
category: Mental
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: ReplacementAccent
|
||||
accent: liar
|
||||
|
||||
- type: trait
|
||||
id: Pacifist
|
||||
name: trait-pacifist-name
|
||||
description: trait-pacifist-desc
|
||||
category: Mental
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Pacified
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
- type: trait
|
||||
id: ScottishAccent
|
||||
name: trait-scottish-accent-name
|
||||
description: trait-scottish-accent-desc
|
||||
traitGear: BagpipeInstrument
|
||||
category: SpeechTraits
|
||||
cost: 2 # Changed weight to 2 in accordance with other accents
|
||||
components:
|
||||
- type: ScottishAccent
|
||||
|
||||
# "New" Accents for more character variety
|
||||
|
||||
- type: trait
|
||||
id: FrenchAccent
|
||||
name: trait-french-accent-name
|
||||
description: trait-french-accent-desc
|
||||
category: SpeechTraits
|
||||
cost: 2 # Changed weight to 2 in accordance with other accents
|
||||
components:
|
||||
- type: FrenchAccent
|
||||
|
||||
- type: trait
|
||||
id: SpanishAccent
|
||||
name: trait-spanish-accent-name
|
||||
description: trait-spanish-accent-desc
|
||||
category: SpeechTraits
|
||||
cost: 2 # Changed weight to 2 in accordance with other accents
|
||||
components:
|
||||
- type: SpanishAccent
|
||||
|
||||
- type: trait
|
||||
id: MobsterAccent
|
||||
name: trait-mobster-accent-name
|
||||
description: trait-mobster-accent-desc
|
||||
category: SpeechTraits
|
||||
cost: 2 # Changed weight to 2 in accordance with other accents
|
||||
components:
|
||||
- type: MobsterAccent
|
||||
|
||||
- type: trait
|
||||
id: IrishAccent
|
||||
name: trait-irish-accent-name
|
||||
description: trait-irish-accent-desc
|
||||
category: SpeechTraits
|
||||
cost: 2 # Changed weight to 2 in accordance with other accents
|
||||
components:
|
||||
- type: IrishAccent
|
||||
|
|
@ -1,11 +1,174 @@
|
|||
## If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly!
|
||||
|
||||
- type: trait
|
||||
id: PirateAccent
|
||||
name: trait-pirate-accent-name
|
||||
description: trait-pirate-accent-desc
|
||||
category: Accents
|
||||
cost: 2
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: PirateAccent
|
||||
|
||||
- type: trait
|
||||
id: SouthernAccent
|
||||
name: trait-southern-name
|
||||
description: trait-southern-desc
|
||||
category: Accents
|
||||
cost: 2
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: SouthernAccent
|
||||
|
||||
- type: trait
|
||||
id: CowboyAccent
|
||||
name: trait-cowboy-name
|
||||
description: trait-cowboy-desc
|
||||
category: Accents
|
||||
cost: 2
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: ReplacementAccent
|
||||
accent: cowboy
|
||||
|
||||
- type: trait
|
||||
id: ItalianAccent
|
||||
name: trait-italian-name
|
||||
description: trait-italian-desc
|
||||
category: Accents
|
||||
cost: 2
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: ReplacementAccent
|
||||
accent: italian
|
||||
|
||||
- type: trait
|
||||
id: Muted
|
||||
name: trait-muted-name
|
||||
description: trait-muted-desc
|
||||
category: Accents
|
||||
cost: 2
|
||||
conditions:
|
||||
- !type:HasCompCondition
|
||||
component: BorgChassis
|
||||
invert: true
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Muted
|
||||
|
||||
- type: trait
|
||||
id: FrenchAccent
|
||||
name: trait-french-accent-name
|
||||
description: trait-french-accent-desc
|
||||
category: Accents
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: FrenchAccent
|
||||
|
||||
- type: trait
|
||||
id: SpanishAccent
|
||||
name: trait-spanish-accent-name
|
||||
description: trait-spanish-accent-desc
|
||||
category: Accents
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: SpanishAccent
|
||||
|
||||
- type: trait
|
||||
id: IrishAccent
|
||||
name: trait-irish-accent-name
|
||||
description: trait-irish-accent-desc
|
||||
category: Accents
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: IrishAccent
|
||||
|
||||
- type: trait
|
||||
id: MobsterAccent
|
||||
name: trait-mobster-accent-name
|
||||
description: trait-mobster-accent-desc
|
||||
category: Accents
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: MobsterAccent
|
||||
|
||||
- type: trait
|
||||
id: ScottishAccent
|
||||
name: trait-scottish-accent-name
|
||||
description: trait-scottish-accent-desc
|
||||
category: Accents
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: ScottishAccent
|
||||
|
||||
- type: trait
|
||||
id: Accentless
|
||||
name: trait-accentless-name
|
||||
description: trait-accentless-desc
|
||||
category: Accents
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Accentless
|
||||
removes:
|
||||
- type: LizardAccent
|
||||
- type: MothAccent
|
||||
- type: ReplacementAccent
|
||||
accent: dwarf
|
||||
|
||||
- type: trait
|
||||
id: SocialAnxiety
|
||||
name: trait-socialanxiety-name
|
||||
description: trait-socialanxiety-desc
|
||||
category: Accents
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: StutteringAccent
|
||||
matchRandomProb: 0.1
|
||||
fourRandomProb: 0
|
||||
threeRandomProb: 0
|
||||
cutRandomProb: 0
|
||||
|
||||
- type: trait
|
||||
id: FrontalLisp
|
||||
name: trait-frontal-lisp-name
|
||||
description: trait-frontal-lisp-desc
|
||||
category: Accents
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: FrontalLisp
|
||||
|
||||
- type: trait
|
||||
id: Hushed
|
||||
name: trait-hushed-name
|
||||
description: trait-hushed-desc
|
||||
category: SpeechTraits
|
||||
category: Accents
|
||||
cost: 0
|
||||
blacklist:
|
||||
components:
|
||||
- BorgChassis
|
||||
conditions:
|
||||
- !type:HasCompCondition
|
||||
component: BorgChassis
|
||||
invert: true
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Hushed
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
- type: traitCategory
|
||||
id: Disabilities
|
||||
name: trait-category-disabilities
|
||||
priority: 0
|
||||
accentColor: "#64748b" # Slate / muted gray
|
||||
|
||||
- type: traitCategory
|
||||
id: Mental
|
||||
name: trait-category-mental
|
||||
priority: 20
|
||||
accentColor: "#a855f7" # Purple
|
||||
|
||||
- type: traitCategory
|
||||
id: Accents
|
||||
name: trait-category-accents
|
||||
priority: 30
|
||||
maxTraits: 3
|
||||
maxPoints: 2
|
||||
accentColor: "#38bdf8" # Light blue
|
||||
|
||||
- type: traitCategory
|
||||
id: Medical
|
||||
name: trait-category-medical
|
||||
priority: 40
|
||||
accentColor: "#ef4444" # Red
|
||||
Loading…
Reference in New Issue