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();
|
_profileEditor.RefreshSpecies();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.WasModified<TraitPrototype>())
|
// if (obj.WasModified<TraitPrototype>()) // DeltaV - Refreshed in TraitsTab
|
||||||
{
|
// {
|
||||||
_profileEditor.RefreshTraits();
|
// _profileEditor.RefreshTraits();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
|
xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
|
||||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||||
xmlns:ui="clr-namespace:Content.Client.Lobby.UI"
|
xmlns:ui="clr-namespace:Content.Client.Lobby.UI"
|
||||||
|
xmlns:traits="clr-namespace:Content.Client._DV.Traits.UI"
|
||||||
HorizontalExpand="True">
|
HorizontalExpand="True">
|
||||||
<!-- Left side -->
|
<!-- Left side -->
|
||||||
<BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
|
<BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
|
||||||
|
|
@ -133,12 +134,15 @@
|
||||||
<BoxContainer Name="AntagList" Orientation="Vertical" />
|
<BoxContainer Name="AntagList" Orientation="Vertical" />
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
<BoxContainer Orientation="Vertical" Margin="10">
|
<!-- DV - Traits -->
|
||||||
<!-- Traits -->
|
<!-- <BoxContainer Orientation="Vertical" Margin="10"> -->
|
||||||
<ScrollContainer VerticalExpand="True">
|
<!-- ~1~ Traits @1@ -->
|
||||||
<BoxContainer Name="TraitsList" Orientation="Vertical" />
|
<!-- <ScrollContainer VerticalExpand="True"> -->
|
||||||
</ScrollContainer>
|
<!-- <BoxContainer Name="TraitsList" Orientation="Vertical" /> -->
|
||||||
</BoxContainer>
|
<!-- </ScrollContainer> -->
|
||||||
|
<!-- </BoxContainer> -->
|
||||||
|
<traits:TraitsTab Name="Traits"/>
|
||||||
|
<!-- End DV -->
|
||||||
<BoxContainer Name="MarkingsTab" Orientation="Vertical" Margin="10">
|
<BoxContainer Name="MarkingsTab" Orientation="Vertical" Margin="10">
|
||||||
<!-- Markings -->
|
<!-- Markings -->
|
||||||
<ScrollContainer VerticalExpand="True">
|
<ScrollContainer VerticalExpand="True">
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ using System.Globalization;
|
||||||
using Content.Client._CD.Records.UI;
|
using Content.Client._CD.Records.UI;
|
||||||
using Content.Shared._CD.Records;
|
using Content.Shared._CD.Records;
|
||||||
// End CD - Character Records
|
// End CD - Character Records
|
||||||
|
using Content.Shared._DV.Traits; // DV - Traits
|
||||||
|
|
||||||
namespace Content.Client.Lobby.UI
|
namespace Content.Client.Lobby.UI
|
||||||
{
|
{
|
||||||
|
|
@ -180,6 +181,8 @@ namespace Content.Client.Lobby.UI
|
||||||
Save?.Invoke();
|
Save?.Invoke();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Traits.OnTraitsChanged += OnTraitsSelectionChanged; // DeltaV
|
||||||
|
|
||||||
#region Left
|
#region Left
|
||||||
|
|
||||||
#region Name
|
#region Name
|
||||||
|
|
@ -239,7 +242,6 @@ namespace Content.Client.Lobby.UI
|
||||||
{
|
{
|
||||||
SpeciesButton.SelectId(args.Id);
|
SpeciesButton.SelectId(args.Id);
|
||||||
SetSpecies(_species[args.Id].ID);
|
SetSpecies(_species[args.Id].ID);
|
||||||
RefreshTraits(); // DeltaV - Allows for hiding traits
|
|
||||||
UpdateHairPickers();
|
UpdateHairPickers();
|
||||||
OnSkinColorOnValueChanged();
|
OnSkinColorOnValueChanged();
|
||||||
};
|
};
|
||||||
|
|
@ -462,7 +464,7 @@ namespace Content.Client.Lobby.UI
|
||||||
|
|
||||||
TabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
|
TabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
|
||||||
|
|
||||||
RefreshTraits();
|
// RefreshTraits(); // DeltaV
|
||||||
|
|
||||||
#region Markings
|
#region Markings
|
||||||
|
|
||||||
|
|
@ -515,6 +517,57 @@ namespace Content.Client.Lobby.UI
|
||||||
IsDirty = false;
|
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>
|
/// <summary>
|
||||||
/// Refreshes the flavor text editor status.
|
/// Refreshes the flavor text editor status.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -549,122 +602,122 @@ namespace Content.Client.Lobby.UI
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Refreshes traits selector
|
/// Refreshes traits selector
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RefreshTraits()
|
// public void RefreshTraits()
|
||||||
{
|
// {
|
||||||
TraitsList.RemoveAllChildren();
|
// TraitsList.RemoveAllChildren();
|
||||||
|
//
|
||||||
var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
|
// var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
|
||||||
TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
|
// TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
|
||||||
|
//
|
||||||
if (traits.Count < 1)
|
// if (traits.Count < 1)
|
||||||
{
|
// {
|
||||||
TraitsList.AddChild(new Label
|
// TraitsList.AddChild(new Label
|
||||||
{
|
// {
|
||||||
Text = Loc.GetString("humanoid-profile-editor-no-traits"),
|
// Text = Loc.GetString("humanoid-profile-editor-no-traits"),
|
||||||
FontColorOverride = Color.Gray,
|
// FontColorOverride = Color.Gray,
|
||||||
});
|
// });
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Setup model
|
// // Setup model
|
||||||
Dictionary<string, List<string>> traitGroups = new();
|
// Dictionary<string, List<string>> traitGroups = new();
|
||||||
List<string> defaultTraits = new();
|
// List<string> defaultTraits = new();
|
||||||
traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
|
// traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
|
||||||
|
//
|
||||||
foreach (var trait in traits)
|
// foreach (var trait in traits)
|
||||||
{
|
// {
|
||||||
// Begin DeltaV Additions - Species trait exclusion
|
// // Begin DeltaV Additions - Species trait exclusion
|
||||||
if (Profile?.Species is { } selectedSpecies && trait.ExcludedSpecies.Contains(selectedSpecies))
|
// if (Profile?.Species is { } selectedSpecies && trait.ExcludedSpecies.Contains(selectedSpecies))
|
||||||
{
|
// {
|
||||||
Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
// Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
// End DeltaV Additions
|
// // End DeltaV Additions
|
||||||
|
//
|
||||||
if (trait.Category == null)
|
// if (trait.Category == null)
|
||||||
{
|
// {
|
||||||
defaultTraits.Add(trait.ID);
|
// defaultTraits.Add(trait.ID);
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (!_prototypeManager.HasIndex(trait.Category))
|
// if (!_prototypeManager.HasIndex(trait.Category))
|
||||||
continue;
|
// continue;
|
||||||
|
//
|
||||||
var group = traitGroups.GetOrNew(trait.Category);
|
// var group = traitGroups.GetOrNew(trait.Category);
|
||||||
group.Add(trait.ID);
|
// group.Add(trait.ID);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Create UI view from model
|
// // Create UI view from model
|
||||||
foreach (var (categoryId, categoryTraits) in traitGroups)
|
// foreach (var (categoryId, categoryTraits) in traitGroups)
|
||||||
{
|
// {
|
||||||
TraitCategoryPrototype? category = null;
|
// TraitCategoryPrototype? category = null;
|
||||||
|
//
|
||||||
if (categoryId != TraitCategoryPrototype.Default)
|
// if (categoryId != TraitCategoryPrototype.Default)
|
||||||
{
|
// {
|
||||||
category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
|
// category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
|
||||||
// Label
|
// // Label
|
||||||
TraitsList.AddChild(new Label
|
// TraitsList.AddChild(new Label
|
||||||
{
|
// {
|
||||||
Text = Loc.GetString(category.Name),
|
// Text = Loc.GetString(category.Name),
|
||||||
Margin = new Thickness(0, 10, 0, 0),
|
// Margin = new Thickness(0, 10, 0, 0),
|
||||||
StyleClasses = { StyleClass.LabelHeading },
|
// StyleClasses = { StyleClass.LabelHeading },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
List<TraitPreferenceSelector?> selectors = new();
|
// List<TraitPreferenceSelector?> selectors = new();
|
||||||
var selectionCount = 0;
|
// var selectionCount = 0;
|
||||||
|
//
|
||||||
foreach (var traitProto in categoryTraits)
|
// foreach (var traitProto in categoryTraits)
|
||||||
{
|
// {
|
||||||
var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
|
// var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
|
||||||
var selector = new TraitPreferenceSelector(trait);
|
// var selector = new TraitPreferenceSelector(trait);
|
||||||
|
//
|
||||||
selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
|
// selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
|
||||||
if (selector.Preference)
|
// if (selector.Preference)
|
||||||
selectionCount += trait.Cost;
|
// selectionCount += trait.Cost;
|
||||||
|
//
|
||||||
selector.PreferenceChanged += preference =>
|
// selector.PreferenceChanged += preference =>
|
||||||
{
|
// {
|
||||||
if (preference)
|
// if (preference)
|
||||||
{
|
// {
|
||||||
Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
|
// Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
|
||||||
}
|
// }
|
||||||
else
|
// else
|
||||||
{
|
// {
|
||||||
Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
// Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SetDirty();
|
// SetDirty();
|
||||||
RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
|
// RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
|
||||||
};
|
// };
|
||||||
selectors.Add(selector);
|
// selectors.Add(selector);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Selection counter
|
// // Selection counter
|
||||||
if (category is { MaxTraitPoints: >= 0 })
|
// if (category is { MaxTraitPoints: >= 0 })
|
||||||
{
|
// {
|
||||||
TraitsList.AddChild(new Label
|
// TraitsList.AddChild(new Label
|
||||||
{
|
// {
|
||||||
Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
|
// Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
|
||||||
FontColorOverride = Color.Gray
|
// FontColorOverride = Color.Gray
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
foreach (var selector in selectors)
|
// foreach (var selector in selectors)
|
||||||
{
|
// {
|
||||||
if (selector == null)
|
// if (selector == null)
|
||||||
continue;
|
// continue;
|
||||||
|
//
|
||||||
if (category is { MaxTraitPoints: >= 0 } &&
|
// if (category is { MaxTraitPoints: >= 0 } &&
|
||||||
selector.Cost + selectionCount > category.MaxTraitPoints)
|
// selector.Cost + selectionCount > category.MaxTraitPoints)
|
||||||
{
|
// {
|
||||||
selector.Checkbox.Label.FontColorOverride = Color.Red;
|
// selector.Checkbox.Label.FontColorOverride = Color.Red;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
TraitsList.AddChild(selector);
|
// TraitsList.AddChild(selector);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Refreshes the species selector.
|
/// Refreshes the species selector.
|
||||||
|
|
@ -845,11 +898,13 @@ namespace Content.Client.Lobby.UI
|
||||||
_recordsTab.Update(profile);
|
_recordsTab.Update(profile);
|
||||||
// End CD - Character Records
|
// End CD - Character Records
|
||||||
|
|
||||||
|
UpdateTraitsSelection(); // DeltaV - Traits
|
||||||
|
|
||||||
RefreshAntags();
|
RefreshAntags();
|
||||||
RefreshJobs();
|
RefreshJobs();
|
||||||
RefreshLoadouts();
|
RefreshLoadouts();
|
||||||
RefreshSpecies();
|
RefreshSpecies();
|
||||||
RefreshTraits();
|
// RefreshTraits(); // DeltaV
|
||||||
RefreshFlavorText();
|
RefreshFlavorText();
|
||||||
ReloadPreview();
|
ReloadPreview();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,22 +19,24 @@ public sealed partial class TraitPreferenceSelector : Control
|
||||||
|
|
||||||
public event Action<bool>? PreferenceChanged;
|
public event Action<bool>? PreferenceChanged;
|
||||||
|
|
||||||
public TraitPreferenceSelector(TraitPrototype trait)
|
// 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
|
||||||
RobustXamlLoader.Load(this);
|
// public TraitPreferenceSelector(TraitPrototype trait)
|
||||||
|
// {
|
||||||
var text = trait.Cost != 0 ? $"[{trait.Cost}] " : "";
|
// RobustXamlLoader.Load(this);
|
||||||
text += Loc.GetString(trait.Name);
|
//
|
||||||
|
// var text = trait.Cost != 0 ? $"[{trait.Cost}] " : "";
|
||||||
Cost = trait.Cost;
|
// text += Loc.GetString(trait.Name);
|
||||||
Checkbox.Text = text;
|
//
|
||||||
Checkbox.OnToggled += OnCheckBoxToggled;
|
// Cost = trait.Cost;
|
||||||
|
// Checkbox.Text = text;
|
||||||
if (trait.Description is { } desc)
|
// Checkbox.OnToggled += OnCheckBoxToggled;
|
||||||
{
|
//
|
||||||
Checkbox.ToolTip = Loc.GetString(desc);
|
// if (trait.Description is { } desc)
|
||||||
}
|
// {
|
||||||
}
|
// Checkbox.ToolTip = Loc.GetString(desc);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
|
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 Robust.Shared.Prototypes;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.Traits.Assorted; // DV
|
||||||
|
|
||||||
namespace Content.Server.Cloning;
|
namespace Content.Server.Cloning;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ using Robust.Shared.Utility;
|
||||||
using Content.Server._CD.Records; // CD - Character Records
|
using Content.Server._CD.Records; // CD - Character Records
|
||||||
using Content.Shared._CD.Records; // CD - Character Records
|
using Content.Shared._CD.Records; // CD - Character Records
|
||||||
using Content.Shared._DV.Tips; // DV - Tips
|
using Content.Shared._DV.Tips; // DV - Tips
|
||||||
|
using Content.Shared._DV.Traits; // DV - Traits
|
||||||
|
|
||||||
namespace Content.Server.Database
|
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.GameTicking; // DeltaV - Traits rework
|
||||||
using Content.Shared.Hands.Components;
|
// using Content.Shared.Hands.Components;
|
||||||
using Content.Shared.Hands.EntitySystems;
|
// using Content.Shared.Hands.EntitySystems;
|
||||||
using Content.Shared.Roles;
|
// using Content.Shared.Roles;
|
||||||
using Content.Shared.Traits;
|
// using Content.Shared.Traits;
|
||||||
using Content.Shared.Whitelist;
|
// using Content.Shared.Whitelist;
|
||||||
using Robust.Shared.Prototypes;
|
// using Robust.Shared.Prototypes;
|
||||||
|
//
|
||||||
namespace Content.Server.Traits;
|
// namespace Content.Server.Traits;
|
||||||
|
//
|
||||||
public sealed class TraitSystem : EntitySystem
|
// public sealed class TraitSystem : EntitySystem
|
||||||
{
|
// {
|
||||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
// [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
[Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!;
|
// [Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!;
|
||||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
// [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||||
|
//
|
||||||
public override void Initialize()
|
// public override void Initialize()
|
||||||
{
|
// {
|
||||||
base.Initialize();
|
// base.Initialize();
|
||||||
|
//
|
||||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete);
|
// SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// When the player is spawned in, add all trait components selected during character creation
|
// // When the player is spawned in, add all trait components selected during character creation
|
||||||
private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
|
// private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
|
||||||
{
|
// {
|
||||||
// Check if player's job allows to apply traits
|
// // Check if player's job allows to apply traits
|
||||||
if (args.JobId == null ||
|
// if (args.JobId == null ||
|
||||||
!_prototypeManager.Resolve<JobPrototype>(args.JobId, out var protoJob) ||
|
// !_prototypeManager.Resolve<JobPrototype>(args.JobId, out var protoJob) ||
|
||||||
!protoJob.ApplyTraits)
|
// !protoJob.ApplyTraits)
|
||||||
{
|
// {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
foreach (var traitId in args.Profile.TraitPreferences)
|
// foreach (var traitId in args.Profile.TraitPreferences)
|
||||||
{
|
// {
|
||||||
if (!_prototypeManager.TryIndex<TraitPrototype>(traitId, out var traitPrototype))
|
// if (!_prototypeManager.TryIndex<TraitPrototype>(traitId, out var traitPrototype))
|
||||||
{
|
// {
|
||||||
Log.Error($"No trait found with ID {traitId}!");
|
// Log.Error($"No trait found with ID {traitId}!");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (_whitelistSystem.IsWhitelistFail(traitPrototype.Whitelist, args.Mob) ||
|
// if (_whitelistSystem.IsWhitelistFail(traitPrototype.Whitelist, args.Mob) ||
|
||||||
_whitelistSystem.IsBlacklistPass(traitPrototype.Blacklist, args.Mob))
|
// _whitelistSystem.IsBlacklistPass(traitPrototype.Blacklist, args.Mob))
|
||||||
continue;
|
// continue;
|
||||||
|
//
|
||||||
// Add all components required by the prototype
|
// // Add all components required by the prototype
|
||||||
EntityManager.AddComponents(args.Mob, traitPrototype.Components, false);
|
// EntityManager.AddComponents(args.Mob, traitPrototype.Components, false);
|
||||||
|
//
|
||||||
// Begin DeltaV - Add overridden components
|
// // Begin DeltaV - Add overridden components
|
||||||
if(traitPrototype.OverriddenComponents != null)
|
// if(traitPrototype.OverriddenComponents != null)
|
||||||
EntityManager.AddComponents(args.Mob, traitPrototype.OverriddenComponents, true);
|
// EntityManager.AddComponents(args.Mob, traitPrototype.OverriddenComponents, true);
|
||||||
// End DeltaV
|
// // End DeltaV
|
||||||
|
//
|
||||||
// Add item required by the trait
|
// // Add item required by the trait
|
||||||
if (traitPrototype.TraitGear == null)
|
// if (traitPrototype.TraitGear == null)
|
||||||
continue;
|
// continue;
|
||||||
|
//
|
||||||
if (!TryComp(args.Mob, out HandsComponent? handsComponent))
|
// if (!TryComp(args.Mob, out HandsComponent? handsComponent))
|
||||||
continue;
|
// continue;
|
||||||
|
//
|
||||||
var coords = Transform(args.Mob).Coordinates;
|
// var coords = Transform(args.Mob).Coordinates;
|
||||||
var inhandEntity = Spawn(traitPrototype.TraitGear, coords);
|
// var inhandEntity = Spawn(traitPrototype.TraitGear, coords);
|
||||||
_sharedHandsSystem.TryPickup(args.Mob,
|
// _sharedHandsSystem.TryPickup(args.Mob,
|
||||||
inhandEntity,
|
// inhandEntity,
|
||||||
checkActionBlocker: false,
|
// checkActionBlocker: false,
|
||||||
handsComp: handsComponent);
|
// 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.Serialization;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using Content.Shared._CD.Records; // CD - Character Records
|
using Content.Shared._CD.Records; // CD - Character Records
|
||||||
|
using Content.Shared._DV.Traits; // DeltaV - Traits rework
|
||||||
|
|
||||||
namespace Content.Shared.Preferences
|
namespace Content.Shared.Preferences
|
||||||
{
|
{
|
||||||
|
|
@ -444,12 +445,12 @@ namespace Content.Shared.Preferences
|
||||||
// Category not found so dump it.
|
// Category not found so dump it.
|
||||||
TraitCategoryPrototype? traitCategory = null;
|
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);
|
return new(this);
|
||||||
|
|
||||||
var list = new HashSet<ProtoId<TraitPrototype>>(_traitPreferences) { traitId };
|
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)
|
return new(this)
|
||||||
{
|
{
|
||||||
|
|
@ -470,7 +471,7 @@ namespace Content.Shared.Preferences
|
||||||
count += otherProto.Cost;
|
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);
|
return new(this);
|
||||||
}
|
}
|
||||||
|
|
@ -724,11 +725,11 @@ namespace Content.Shared.Preferences
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Always valid.
|
// Always valid.
|
||||||
if (traitProto.Category == null)
|
// if (traitProto.Category == null) // DeltaV 13/01/26 - Traits rework
|
||||||
{
|
// {
|
||||||
result.Add(trait);
|
// result.Add(trait);
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// No category so dump it.
|
// No category so dump it.
|
||||||
if (!protoManager.Resolve(traitProto.Category, out var category))
|
if (!protoManager.Resolve(traitProto.Category, out var category))
|
||||||
|
|
@ -738,7 +739,7 @@ namespace Content.Shared.Preferences
|
||||||
existing += traitProto.Cost;
|
existing += traitProto.Cost;
|
||||||
|
|
||||||
// Too expensive.
|
// Too expensive.
|
||||||
if (existing > category.MaxTraitPoints)
|
if (existing > category.MaxPoints) // DeltaV 13/01/26 - Traits: Was MaxTraitPoints
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
groups[category.ID] = existing;
|
groups[category.ID] = existing;
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
using Robust.Shared.Prototypes;
|
// using Robust.Shared.Prototypes; // DeltaV - Traits rework
|
||||||
|
//
|
||||||
namespace Content.Shared.Traits;
|
// namespace Content.Shared.Traits;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Traits category with general settings. Allows you to limit the number of taken traits in one category
|
// /// Traits category with general settings. Allows you to limit the number of taken traits in one category
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[Prototype]
|
// [Prototype]
|
||||||
public sealed partial class TraitCategoryPrototype : IPrototype
|
// public sealed partial class TraitCategoryPrototype : IPrototype
|
||||||
{
|
// {
|
||||||
public const string Default = "Default";
|
// public const string Default = "Default";
|
||||||
|
//
|
||||||
[ViewVariables]
|
// [ViewVariables]
|
||||||
[IdDataField]
|
// [IdDataField]
|
||||||
public string ID { get; private set; } = default!;
|
// public string ID { get; private set; } = default!;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Name of the trait category displayed in the UI
|
// /// Name of the trait category displayed in the UI
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public LocId Name { get; private set; } = string.Empty;
|
// public LocId Name { get; private set; } = string.Empty;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// The maximum number of traits that can be taken in this category.
|
// /// The maximum number of traits that can be taken in this category.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public int? MaxTraitPoints;
|
// public int? MaxTraitPoints;
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,76 @@
|
||||||
using Content.Shared.Whitelist;
|
// using Content.Shared.Whitelist; // DeltaV - Traits rework
|
||||||
using Robust.Shared.Prototypes;
|
// using Robust.Shared.Prototypes;
|
||||||
using Content.Shared.Humanoid.Prototypes; // DeltaV - Trait species hiding
|
// using Content.Shared.Humanoid.Prototypes; // DeltaV - Trait species hiding
|
||||||
|
//
|
||||||
namespace Content.Shared.Traits;
|
// namespace Content.Shared.Traits;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Describes a trait.
|
// /// Describes a trait.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[Prototype]
|
// [Prototype]
|
||||||
public sealed partial class TraitPrototype : IPrototype
|
// public sealed partial class TraitPrototype : IPrototype
|
||||||
{
|
// {
|
||||||
[ViewVariables]
|
// [ViewVariables]
|
||||||
[IdDataField]
|
// [IdDataField]
|
||||||
public string ID { get; private set; } = default!;
|
// public string ID { get; private set; } = default!;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// The name of this trait.
|
// /// The name of this trait.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public LocId Name { get; private set; } = string.Empty;
|
// public LocId Name { get; private set; } = string.Empty;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// The description of this trait.
|
// /// The description of this trait.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public LocId? Description { get; private set; }
|
// public LocId? Description { get; private set; }
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Don't apply this trait to entities this whitelist IS NOT valid for.
|
// /// Don't apply this trait to entities this whitelist IS NOT valid for.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public EntityWhitelist? Whitelist;
|
// public EntityWhitelist? Whitelist;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Don't apply this trait to entities this whitelist IS valid for. (hence, a blacklist)
|
// /// Don't apply this trait to entities this whitelist IS valid for. (hence, a blacklist)
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public EntityWhitelist? Blacklist;
|
// public EntityWhitelist? Blacklist;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// The components that get added to the player, when they pick this trait.
|
// /// The components that get added to the player, when they pick this trait.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public ComponentRegistry Components { get; private set; } = default!;
|
// public ComponentRegistry Components { get; private set; } = default!;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// DeltaV - Components that get added to the player, overriding any existing instances of the component if they exist.
|
// /// DeltaV - Components that get added to the player, overriding any existing instances of the component if they exist.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public ComponentRegistry? OverriddenComponents { get; private set; }
|
// public ComponentRegistry? OverriddenComponents { get; private set; }
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Gear that is given to the player, when they pick this trait.
|
// /// Gear that is given to the player, when they pick this trait.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public EntProtoId? TraitGear;
|
// public EntProtoId? TraitGear;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Trait Price. If negative number, points will be added.
|
// /// Trait Price. If negative number, points will be added.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public int Cost = 0;
|
// public int Cost = 0;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Adds a trait to a category, allowing you to limit the selection of some traits to the settings of that category.
|
// /// Adds a trait to a category, allowing you to limit the selection of some traits to the settings of that category.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public ProtoId<TraitCategoryPrototype>? Category;
|
// public ProtoId<TraitCategoryPrototype>? Category;
|
||||||
|
//
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// DeltaV - Hides traits from specific species
|
// /// DeltaV - Hides traits from specific species
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
[DataField]
|
// [DataField]
|
||||||
public HashSet<ProtoId<SpeciesPrototype>> ExcludedSpecies = new();
|
// public HashSet<ProtoId<SpeciesPrototype>> ExcludedSpecies = new();
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,23 @@ public sealed partial class DCCVars
|
||||||
public static readonly CVarDef<int> YearOffset =
|
public static readonly CVarDef<int> YearOffset =
|
||||||
CVarDef.Create("game.current_year_offset", 550, CVar.SERVERONLY);
|
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
|
* 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}]
|
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-speech = Speech traits
|
||||||
trait-category-quirks = Quirks
|
trait-category-quirks = Quirks
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
- type: traitCategory
|
# DELTA-V
|
||||||
id: Disabilities
|
# Due to the custom trait system, all traits have been moved under \Resources\Prototypes\_DV\Traits
|
||||||
name: trait-category-disabilities
|
# If you want to add a new one, do it there
|
||||||
|
|
||||||
- type: traitCategory
|
#- type: traitCategory
|
||||||
id: SpeechTraits
|
# id: Disabilities
|
||||||
name: trait-category-speech
|
# name: trait-category-disabilities
|
||||||
maxTraitPoints: 2
|
#
|
||||||
|
#- type: traitCategory
|
||||||
- type: traitCategory
|
# id: SpeechTraits
|
||||||
id: Quirks
|
# name: trait-category-speech
|
||||||
name: trait-category-quirks
|
# 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!
|
# 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
|
#- type: trait
|
||||||
id: Blindness
|
# id: Blindness
|
||||||
name: trait-blindness-name
|
# name: trait-blindness-name
|
||||||
description: trait-blindness-desc
|
# description: trait-blindness-desc
|
||||||
traitGear: WhiteCane
|
# traitGear: WhiteCane
|
||||||
category: Disabilities
|
# category: Disabilities
|
||||||
whitelist:
|
# whitelist:
|
||||||
components:
|
# components:
|
||||||
- Blindable
|
# - Blindable
|
||||||
components:
|
# components:
|
||||||
- type: PermanentBlindness
|
# - type: PermanentBlindness
|
||||||
|
#
|
||||||
- type: trait
|
#- type: trait
|
||||||
id: PoorVision
|
# id: PoorVision
|
||||||
name: trait-poor-vision-name
|
# name: trait-poor-vision-name
|
||||||
description: trait-poor-vision-desc
|
# description: trait-poor-vision-desc
|
||||||
traitGear: ClothingEyesGlasses
|
# traitGear: ClothingEyesGlasses
|
||||||
category: Disabilities
|
# category: Disabilities
|
||||||
whitelist:
|
# whitelist:
|
||||||
components:
|
# components:
|
||||||
- Blindable
|
# - Blindable
|
||||||
components:
|
# components:
|
||||||
- type: PermanentBlindness
|
# - type: PermanentBlindness
|
||||||
blindness: 4
|
# blindness: 4
|
||||||
|
#
|
||||||
- type: trait
|
#- type: trait
|
||||||
id: Narcolepsy
|
# id: Narcolepsy
|
||||||
name: trait-narcolepsy-name
|
# name: trait-narcolepsy-name
|
||||||
description: trait-narcolepsy-desc
|
# description: trait-narcolepsy-desc
|
||||||
category: Disabilities
|
# category: Disabilities
|
||||||
components:
|
# components:
|
||||||
- type: Narcolepsy
|
# - type: Narcolepsy
|
||||||
maxTimeBetweenIncidents: 600
|
# maxTimeBetweenIncidents: 600
|
||||||
minTimeBetweenIncidents: 300
|
# minTimeBetweenIncidents: 300
|
||||||
maxDurationOfIncident: 30
|
# maxDurationOfIncident: 30
|
||||||
minDurationOfIncident: 10
|
# minDurationOfIncident: 10
|
||||||
|
#
|
||||||
- type: trait
|
#- type: trait
|
||||||
id: Unrevivable
|
# id: Unrevivable
|
||||||
name: trait-unrevivable-name
|
# name: trait-unrevivable-name
|
||||||
description: trait-unrevivable-desc
|
# description: trait-unrevivable-desc
|
||||||
category: Disabilities
|
# category: Disabilities
|
||||||
components:
|
# components:
|
||||||
- type: Unrevivable
|
# - type: Unrevivable
|
||||||
cloneable: true
|
# cloneable: true
|
||||||
|
#
|
||||||
- type: trait
|
#- type: trait
|
||||||
id: Monochromacy
|
# id: Monochromacy
|
||||||
name: trait-monochromacy-name
|
# name: trait-monochromacy-name
|
||||||
description: trait-monochromacy-desc
|
# description: trait-monochromacy-desc
|
||||||
category: Disabilities
|
# category: Disabilities
|
||||||
components:
|
# components:
|
||||||
- type: BlackAndWhiteOverlay
|
# - type: BlackAndWhiteOverlay
|
||||||
|
#
|
||||||
- type: trait
|
#- type: trait
|
||||||
id: Muted
|
# id: Muted
|
||||||
name: trait-muted-name
|
# name: trait-muted-name
|
||||||
description: trait-muted-desc
|
# description: trait-muted-desc
|
||||||
category: Disabilities
|
# category: Disabilities
|
||||||
blacklist:
|
# blacklist:
|
||||||
components:
|
# components:
|
||||||
- BorgChassis
|
# - BorgChassis
|
||||||
components:
|
# components:
|
||||||
- type: Muted
|
# - type: Muted
|
||||||
|
#
|
||||||
- type: trait
|
#- type: trait
|
||||||
id: Paracusia
|
# id: Paracusia
|
||||||
name: trait-paracusia-name
|
# name: trait-paracusia-name
|
||||||
description: trait-paracusia-desc
|
# description: trait-paracusia-desc
|
||||||
category: Disabilities
|
# category: Disabilities
|
||||||
components:
|
# components:
|
||||||
- type: Paracusia
|
# - type: Paracusia
|
||||||
minTimeBetweenIncidents: 0.1
|
# minTimeBetweenIncidents: 0.1
|
||||||
maxTimeBetweenIncidents: 300
|
# maxTimeBetweenIncidents: 300
|
||||||
maxSoundDistance: 7
|
# maxSoundDistance: 7
|
||||||
sounds:
|
# sounds:
|
||||||
collection: Paracusia
|
# collection: Paracusia
|
||||||
|
#
|
||||||
- type: trait
|
#- type: trait
|
||||||
id: PainNumbness
|
# id: PainNumbness
|
||||||
name: trait-painnumbness-name
|
# name: trait-painnumbness-name
|
||||||
description: trait-painnumbness-desc
|
# description: trait-painnumbness-desc
|
||||||
category: Disabilities
|
# category: Disabilities
|
||||||
components:
|
# components:
|
||||||
- type: PainNumbness
|
# - type: PainNumbness
|
||||||
|
#
|
||||||
- type: trait
|
#- type: trait
|
||||||
id: ImpairedMobility
|
# id: ImpairedMobility
|
||||||
name: trait-impaired-mobility-name
|
# name: trait-impaired-mobility-name
|
||||||
description: trait-impaired-mobility-desc
|
# description: trait-impaired-mobility-desc
|
||||||
traitGear: OffsetCane
|
# traitGear: OffsetCane
|
||||||
category: Disabilities
|
# category: Disabilities
|
||||||
components:
|
# components:
|
||||||
- type: ImpairedMobility
|
# - type: ImpairedMobility
|
||||||
|
#
|
||||||
- type: trait
|
#- type: trait
|
||||||
id: Hemophilia
|
# id: Hemophilia
|
||||||
name: trait-hemophilia-name
|
# name: trait-hemophilia-name
|
||||||
description: trait-hemophilia-desc
|
# description: trait-hemophilia-desc
|
||||||
category: Disabilities
|
# category: Disabilities
|
||||||
components:
|
# components:
|
||||||
- type: Hemophilia
|
# - 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
|
## 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!
|
||||||
id: Pacifist
|
#
|
||||||
name: trait-pacifist-name
|
#- type: trait
|
||||||
description: trait-pacifist-desc
|
# id: Pacifist
|
||||||
category: Quirks
|
# name: trait-pacifist-name
|
||||||
components:
|
# description: trait-pacifist-desc
|
||||||
- type: Pacified
|
# category: Quirks
|
||||||
|
# components:
|
||||||
- type: trait
|
# - type: Pacified
|
||||||
id: LightweightDrunk
|
#
|
||||||
name: trait-lightweight-name
|
#- type: trait
|
||||||
description: trait-lightweight-desc
|
# id: LightweightDrunk
|
||||||
category: Quirks
|
# name: trait-lightweight-name
|
||||||
components:
|
# description: trait-lightweight-desc
|
||||||
- type: LightweightDrunk
|
# category: Quirks
|
||||||
boozeStrengthMultiplier: 2
|
# components:
|
||||||
|
# - type: LightweightDrunk
|
||||||
- type: trait
|
# boozeStrengthMultiplier: 2
|
||||||
id: Snoring
|
#
|
||||||
name: trait-snoring-name
|
#- type: trait
|
||||||
description: trait-snoring-desc
|
# id: Snoring
|
||||||
category: Quirks
|
# name: trait-snoring-name
|
||||||
components:
|
# description: trait-snoring-desc
|
||||||
- type: Snoring
|
# 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!
|
# DELTA-V
|
||||||
|
# Due to the custom trait system, all traits have been moved under \Resources\Prototypes\_DV\Traits
|
||||||
# Free
|
# If you want to add a new one, do it there
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
# DELTA-V
|
||||||
category: Quirks
|
# Due to the custom trait system, all traits have been moved under \Resources\Prototypes\_DV\Traits
|
||||||
id: Synthetic
|
# If you want to add a new one, do it there
|
||||||
name: trait-synth-name
|
|
||||||
description: trait-synth-desc
|
#- type: trait
|
||||||
# Begin DeltaV Additions - blacklist IPCs
|
# category: Quirks
|
||||||
blacklist:
|
# id: Synthetic
|
||||||
components:
|
# name: trait-synth-name
|
||||||
- Silicon
|
# description: trait-synth-desc
|
||||||
- BorgChassis
|
# # Begin DeltaV Additions - blacklist IPCs
|
||||||
excludedSpecies:
|
# blacklist:
|
||||||
- IPC
|
# components:
|
||||||
# End DeltaV Additions - blacklist IPCs
|
# - Silicon
|
||||||
components:
|
# - BorgChassis
|
||||||
- type: Synth
|
# 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
|
## 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!
|
||||||
id: Addicted
|
|
||||||
name: trait-addicted-name
|
|
||||||
description: trait-addicted-desc
|
|
||||||
category: Disabilities
|
|
||||||
components:
|
|
||||||
- type: Addicted
|
|
||||||
|
|
||||||
- type: trait
|
- type: trait
|
||||||
id: InPain
|
id: Blindness
|
||||||
name: trait-inpain-name
|
name: trait-blindness-name
|
||||||
description: trait-inpain-desc
|
description: trait-blindness-desc
|
||||||
traitGear: PillCanisterSoretizone
|
|
||||||
category: Disabilities
|
category: Disabilities
|
||||||
components:
|
conditions:
|
||||||
- type: Pain
|
- !type:HasCompCondition
|
||||||
|
component: Blindable
|
||||||
|
effects:
|
||||||
|
- !type:AddCompsEffect
|
||||||
|
components:
|
||||||
|
- type: PermanentBlindness
|
||||||
|
- !type:SpawnItemInHandEffect
|
||||||
|
item: WhiteCane
|
||||||
|
|
||||||
- type: trait
|
- type: trait
|
||||||
id: Unborgable
|
id: PoorVision
|
||||||
name: trait-unborgable-name
|
name: trait-poor-vision-name
|
||||||
description: trait-unborgable-desc
|
description: trait-poor-vision-desc
|
||||||
category: Disabilities
|
category: Disabilities
|
||||||
components:
|
conditions:
|
||||||
- type: Unborgable # Automatically gets moved to the brain
|
- !type:HasCompCondition
|
||||||
|
component: Blindable
|
||||||
|
effects:
|
||||||
|
- !type:AddCompsEffect
|
||||||
|
components:
|
||||||
|
- type: PermanentBlindness
|
||||||
|
blindness: 4
|
||||||
|
- !type:SpawnItemInHandEffect
|
||||||
|
item: ClothingEyesGlasses
|
||||||
|
|
||||||
- type: trait
|
- type: trait
|
||||||
id: Depression
|
id: Monochromacy
|
||||||
name: trait-depression-name
|
name: trait-monochromacy-name
|
||||||
description: trait-depression-desc
|
description: trait-monochromacy-desc
|
||||||
traitGear: PillCanisterNeurozenium
|
|
||||||
category: Disabilities
|
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
|
- type: trait
|
||||||
id: Dysgraphia
|
id: Dysgraphia
|
||||||
name: trait-dysgraphia-name
|
name: trait-dysgraphia-name
|
||||||
description: trait-dysgraphia-desc
|
description: trait-dysgraphia-desc
|
||||||
category: Disabilities
|
category: Disabilities
|
||||||
components:
|
effects:
|
||||||
- type: BlockWriting
|
- !type:AddCompsEffect
|
||||||
|
components:
|
||||||
|
- type: BlockWriting
|
||||||
|
|
||||||
- type: trait
|
- type: trait
|
||||||
id: Redshirt
|
id: PainNumbness
|
||||||
name: trait-redshirt-name
|
name: trait-painnumbness-name
|
||||||
description: trait-redshirt-desc
|
description: trait-painnumbness-desc
|
||||||
category: Disabilities
|
category: Disabilities
|
||||||
components:
|
effects:
|
||||||
- type: Redshirt
|
- !type:AddCompsEffect
|
||||||
overriddenComponents:
|
components:
|
||||||
- type: MobThresholds
|
- type: PainNumbness
|
||||||
thresholds:
|
|
||||||
0: Alive
|
- type: trait
|
||||||
99.9: Critical
|
id: Hemophilia
|
||||||
100: Dead
|
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
|
- type: trait
|
||||||
id: Hushed
|
id: Hushed
|
||||||
name: trait-hushed-name
|
name: trait-hushed-name
|
||||||
description: trait-hushed-desc
|
description: trait-hushed-desc
|
||||||
category: SpeechTraits
|
category: Accents
|
||||||
cost: 0
|
cost: 0
|
||||||
blacklist:
|
conditions:
|
||||||
|
- !type:HasCompCondition
|
||||||
|
component: BorgChassis
|
||||||
|
invert: true
|
||||||
|
effects:
|
||||||
|
- !type:AddCompsEffect
|
||||||
components:
|
components:
|
||||||
- BorgChassis
|
- type: Hushed
|
||||||
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