From a4d16017589e82822c9880b146b8d3dab9621093 Mon Sep 17 00:00:00 2001 From: Ed <96445749+TheShuEd@users.noreply.github.com> Date: Mon, 3 Jun 2024 21:47:06 +0300 Subject: [PATCH] Accent trait limit (#28046) --- .../Lobby/UI/HumanoidProfileEditor.xaml.cs | 99 +++++++++++++---- .../UI/Roles/TraitPreferenceSelector.xaml | 2 +- .../UI/Roles/TraitPreferenceSelector.xaml.cs | 9 +- Content.Server/Traits/TraitSystem.cs | 30 ++--- .../Preferences/HumanoidCharacterProfile.cs | 32 +++++- .../Traits/TraitCategoryPrototype.cs | 26 +++++ Content.Shared/Traits/TraitPrototype.cs | 104 ++++++++++-------- .../ui/humanoid-profile-editor.ftl | 11 +- Resources/Locale/en-US/traits/traits.ftl | 18 ++- Resources/Prototypes/Traits/categories.yml | 8 ++ Resources/Prototypes/Traits/disabilities.yml | 42 ++++--- .../Prototypes/Traits/inconveniences.yml | 18 --- Resources/Prototypes/Traits/neutral.yml | 33 ------ Resources/Prototypes/Traits/speech.yml | 88 +++++++++++++++ 14 files changed, 357 insertions(+), 163 deletions(-) create mode 100644 Content.Shared/Traits/TraitCategoryPrototype.cs create mode 100644 Resources/Prototypes/Traits/categories.yml delete mode 100644 Resources/Prototypes/Traits/inconveniences.yml delete mode 100644 Resources/Prototypes/Traits/neutral.yml create mode 100644 Resources/Prototypes/Traits/speech.yml diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index 53c332c185..ac43fa11a8 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -7,6 +7,7 @@ using Content.Client.Lobby.UI.Loadouts; using Content.Client.Lobby.UI.Roles; using Content.Client.Message; using Content.Client.Players.PlayTimeTracking; +using Content.Client.Stylesheets; using Content.Client.UserInterface.Systems.Guidebook; using Content.Shared.CCVar; using Content.Shared.Clothing; @@ -466,38 +467,96 @@ namespace Content.Client.Lobby.UI var traits = _prototypeManager.EnumeratePrototypes().OrderBy(t => Loc.GetString(t.Name)).ToList(); TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab")); - if (traits.Count > 0) + if (traits.Count < 1) { - foreach (var trait in traits) + TraitsList.AddChild(new Label { - var selector = new TraitPreferenceSelector(trait); + Text = Loc.GetString("humanoid-profile-editor-no-traits"), + FontColorOverride = Color.Gray, + }); + return; + } - if (Profile?.TraitPreferences.Contains(trait.ID) == true) - { - selector.Preference = true; - } - else + //Setup model + Dictionary> model = new(); + List defaultTraits = new(); + model.Add("default", defaultTraits); + + foreach (var trait in traits) + { + if (trait.Category == null) + { + defaultTraits.Add(trait.ID); + continue; + } + + if (!model.ContainsKey(trait.Category)) + { + model.Add(trait.Category, new()); + } + model[trait.Category].Add(trait.ID); + } + + //Create UI view from model + foreach (var (categoryId, traitId) in model) + { + TraitCategoryPrototype? category = null; + if (categoryId != "default") + { + category = _prototypeManager.Index(categoryId); + // Label + TraitsList.AddChild(new Label { - selector.Preference = false; - } + Text = Loc.GetString(category.Name), + Margin = new Thickness(0, 10, 0, 0), + StyleClasses = { StyleBase.StyleClassLabelHeading }, + }); + } + + List selectors = new(); + var selectionCount = 0; + + foreach (var traitProto in traitId) + { + var trait = _prototypeManager.Index(traitProto); + var selector = new TraitPreferenceSelector(trait); + + selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true; + if (selector.Preference) + selectionCount += trait.Cost; selector.PreferenceChanged += preference => { - Profile = Profile?.WithTraitPreference(trait.ID, preference); + Profile = Profile?.WithTraitPreference(trait.ID, categoryId, preference); SetDirty(); + RefreshTraits(); // If too many traits are selected, they will be reset to the real value. }; + selectors.Add(selector); + } - TraitsList.AddChild(selector); + // Selection counter + if (category is { MaxTraitPoints: >= 0 }) + { + TraitsList.AddChild(new Label + { + Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)), + FontColorOverride = Color.Gray + }); } - } - else - { - TraitsList.AddChild(new Label + + foreach (var selector in selectors) { - // TODO: Localise - Text = "No traits available :(", - FontColorOverride = Color.Gray, - }); + if (selector == null) + continue; + + if (category is { MaxTraitPoints: >= 0 } && + selector.Cost + selectionCount > category.MaxTraitPoints) + { + selector.Checkbox.Label.FontColorOverride = Color.Red; + } + + TraitsList.AddChild(selector); + } } } diff --git a/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml b/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml index 18dabe3090..266b4b8eee 100644 --- a/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml +++ b/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml @@ -2,6 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + diff --git a/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml.cs b/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml.cs index 498a5ca4e5..a52a3fa2db 100644 --- a/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml.cs +++ b/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml.cs @@ -9,6 +9,8 @@ namespace Content.Client.Lobby.UI.Roles; [GenerateTypedNameReferences] public sealed partial class TraitPreferenceSelector : Control { + public int Cost; + public bool Preference { get => Checkbox.Pressed; @@ -20,7 +22,12 @@ public sealed partial class TraitPreferenceSelector : Control public TraitPreferenceSelector(TraitPrototype trait) { RobustXamlLoader.Load(this); - Checkbox.Text = Loc.GetString(trait.Name); + + var text = trait.Cost != 0 ? $"[{trait.Cost}] " : ""; + text += Loc.GetString(trait.Name); + + Cost = trait.Cost; + Checkbox.Text = text; Checkbox.OnToggled += OnCheckBoxToggled; if (trait.Description is { } desc) diff --git a/Content.Server/Traits/TraitSystem.cs b/Content.Server/Traits/TraitSystem.cs index 22ee0e4861..f7531f7e2c 100644 --- a/Content.Server/Traits/TraitSystem.cs +++ b/Content.Server/Traits/TraitSystem.cs @@ -38,27 +38,21 @@ public sealed class TraitSystem : EntitySystem continue; // Add all components required by the prototype - foreach (var entry in traitPrototype.Components.Values) - { - if (HasComp(args.Mob, entry.Component.GetType())) - continue; - - var comp = (Component) _serializationManager.CreateCopy(entry.Component, notNullableOverride: true); - comp.Owner = args.Mob; - EntityManager.AddComponent(args.Mob, comp); - } + EntityManager.AddComponents(args.Mob, traitPrototype.Components, false); // Add item required by the trait - if (traitPrototype.TraitGear != null) - { - if (!TryComp(args.Mob, out HandsComponent? handsComponent)) - continue; + if (traitPrototype.TraitGear == null) + continue; - var coords = Transform(args.Mob).Coordinates; - var inhandEntity = EntityManager.SpawnEntity(traitPrototype.TraitGear, coords); - _sharedHandsSystem.TryPickup(args.Mob, inhandEntity, checkActionBlocker: false, - handsComp: handsComponent); - } + if (!TryComp(args.Mob, out HandsComponent? handsComponent)) + continue; + + var coords = Transform(args.Mob).Coordinates; + var inhandEntity = EntityManager.SpawnEntity(traitPrototype.TraitGear, coords); + _sharedHandsSystem.TryPickup(args.Mob, + inhandEntity, + checkActionBlocker: false, + handsComp: handsComponent); } } } diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index f47a3ac3db..bd55bbb40a 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -346,13 +346,43 @@ namespace Content.Shared.Preferences }; } - public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref) + public HumanoidCharacterProfile WithTraitPreference(string traitId, string? categoryId, bool pref) { + var prototypeManager = IoCManager.Resolve(); + var traitProto = prototypeManager.Index(traitId); + + TraitCategoryPrototype? categoryProto = null; + if (categoryId != null && categoryId != "default") + categoryProto = prototypeManager.Index(categoryId); + var list = new HashSet(_traitPreferences); if (pref) { list.Add(traitId); + + if (categoryProto == null || categoryProto.MaxTraitPoints < 0) + { + return new(this) + { + _traitPreferences = list, + }; + } + + var count = 0; + foreach (var trait in list) + { + var traitProtoTemp = prototypeManager.Index(trait); + count += traitProtoTemp.Cost; + } + + if (count > categoryProto.MaxTraitPoints && traitProto.Cost != 0) + { + return new(this) + { + _traitPreferences = _traitPreferences, + }; + } } else { diff --git a/Content.Shared/Traits/TraitCategoryPrototype.cs b/Content.Shared/Traits/TraitCategoryPrototype.cs new file mode 100644 index 0000000000..1da624173a --- /dev/null +++ b/Content.Shared/Traits/TraitCategoryPrototype.cs @@ -0,0 +1,26 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Traits; + +/// +/// Traits category with general settings. Allows you to limit the number of taken traits in one category +/// +[Prototype] +public sealed partial class TraitCategoryPrototype : IPrototype +{ + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// Name of the trait category displayed in the UI + /// + [DataField] + public LocId Name { get; private set; } = string.Empty; + + /// + /// The maximum number of traits that can be taken in this category. If -1, you can take as many traits as you like. + /// + [DataField] + public int MaxTraitPoints = -1; +} diff --git a/Content.Shared/Traits/TraitPrototype.cs b/Content.Shared/Traits/TraitPrototype.cs index 34feb8da22..c79d3cbf30 100644 --- a/Content.Shared/Traits/TraitPrototype.cs +++ b/Content.Shared/Traits/TraitPrototype.cs @@ -1,55 +1,63 @@ using Content.Shared.Whitelist; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -// don't worry about it +namespace Content.Shared.Traits; -namespace Content.Shared.Traits +/// +/// Describes a trait. +/// +[Prototype] +public sealed partial class TraitPrototype : IPrototype { + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// The name of this trait. + /// + [DataField] + public LocId Name { get; private set; } = string.Empty; + + /// + /// The description of this trait. + /// + [DataField] + public LocId? Description { get; private set; } + + /// + /// Don't apply this trait to entities this whitelist IS NOT valid for. + /// + [DataField] + public EntityWhitelist? Whitelist; + /// - /// Describes a trait. - /// - [Prototype("trait")] - public sealed partial class TraitPrototype : IPrototype - { - [ViewVariables] - [IdDataField] - public string ID { get; private set; } = default!; - - /// - /// The name of this trait. - /// - [DataField("name")] - public string Name { get; private set; } = ""; - - /// - /// The description of this trait. - /// - [DataField("description")] - public string? Description { get; private set; } - - /// - /// Don't apply this trait to entities this whitelist IS NOT valid for. - /// - [DataField("whitelist")] - public EntityWhitelist? Whitelist; - - /// - /// Don't apply this trait to entities this whitelist IS valid for. (hence, a blacklist) - /// - [DataField("blacklist")] - public EntityWhitelist? Blacklist; - - /// - /// The components that get added to the player, when they pick this trait. - /// - [DataField("components")] - public ComponentRegistry Components { get; private set; } = default!; - - /// - /// Gear that is given to the player, when they pick this trait. - /// - [DataField("traitGear", required: false, customTypeSerializer:typeof(PrototypeIdSerializer))] - public string? TraitGear; - } + /// Don't apply this trait to entities this whitelist IS valid for. (hence, a blacklist) + /// + [DataField] + public EntityWhitelist? Blacklist; + + /// + /// The components that get added to the player, when they pick this trait. + /// + [DataField] + public ComponentRegistry Components { get; private set; } = default!; + + /// + /// Gear that is given to the player, when they pick this trait. + /// + [DataField] + public EntProtoId? TraitGear; + + /// + /// Trait Price. If negative number, points will be added. + /// + [DataField] + public int Cost = 0; + + /// + /// Adds a trait to a category, allowing you to limit the selection of some traits to the settings of that category. + /// + [DataField] + public ProtoId? Category; } diff --git a/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl b/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl index c7a24d5405..bfdbeb2f14 100644 --- a/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl +++ b/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl @@ -42,7 +42,7 @@ humanoid-profile-editor-department-jobs-label = {$departmentName} jobs humanoid-profile-editor-antags-tab = Antags humanoid-profile-editor-antag-preference-yes-button = Yes humanoid-profile-editor-antag-preference-no-button = No -humanoid-profile-editor-traits-tab = Traits + humanoid-profile-editor-job-priority-high-button = High humanoid-profile-editor-job-priority-medium-button = Medium humanoid-profile-editor-job-priority-low-button = Low @@ -50,3 +50,12 @@ humanoid-profile-editor-job-priority-never-button = Never humanoid-profile-editor-naming-rules-warning = Warning: Offensive or LRP IC names and descriptions will lead to admin intervention on this server. Read our \[Rules\] for more. humanoid-profile-editor-markings-tab = Markings humanoid-profile-editor-flavortext-tab = Description + +# Traits +humanoid-profile-editor-traits-tab = Traits +humanoid-profile-editor-no-traits = No traits available + +humanoid-profile-editor-trait-count-hint = Points available: [{$current}/{$max}] + +trait-category-disabilities = Disabilities +trait-category-speech = Speech traits \ No newline at end of file diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index cbf65308f3..e3aed1c8b2 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -12,7 +12,7 @@ trait-pacifist-desc = You cannot attack or hurt any living beings. permanent-blindness-trait-examined = [color=lightblue]{CAPITALIZE(POSS-ADJ($target))} eyes are glassy and unfocused. It doesn't seem like {SUBJECT($target)} can see you well, if at all.[/color] -trait-lightweight-name = Lightweight Drunk +trait-lightweight-name = Lightweight drunk trait-lightweight-desc = Alcohol has a stronger effect on you trait-muted-name = Muted @@ -24,23 +24,29 @@ trait-paracusia-desc = You hear sounds that aren't really there trait-unrevivable-name = Unrevivable trait-unrevivable-desc = You are unable to be revived by defibrillators. -trait-pirate-accent-name = Pirate Accent +trait-pirate-accent-name = Pirate accent trait-pirate-accent-desc = You can't stop speaking like a pirate! trait-accentless-name = Accentless trait-accentless-desc = You don't have the accent that your species would usually have -trait-frontal-lisp-name = Frontal Lisp +trait-frontal-lisp-name = Frontal lisp trait-frontal-lisp-desc = You thpeak with a lithp -trait-socialanxiety-name = Social Anxiety +trait-socialanxiety-name = Social anxiety trait-socialanxiety-desc = You are anxious when you speak and stutter. -trait-southern-name = Southern Drawl +trait-southern-name = Southern drawl trait-southern-desc = You have a different way of speakin'. trait-snoring-name = Snoring trait-snoring-desc = You will snore while sleeping. trait-liar-name = Pathological liar -trait-liar-desc = You can hardly bring yourself to tell the truth. Sometimes you lie anyway. \ No newline at end of file +trait-liar-desc = You can hardly bring yourself to tell the truth. Sometimes you lie anyway. + +trait-cowboy-name = Cowboy accent +trait-cowboy-desc = You speak with a distinct cowboy accent! + +trait-italian-name = Italian accent +trait-italian-desc = Mamma mia! You seem to have lived in space italy! \ No newline at end of file diff --git a/Resources/Prototypes/Traits/categories.yml b/Resources/Prototypes/Traits/categories.yml new file mode 100644 index 0000000000..a3621648a4 --- /dev/null +++ b/Resources/Prototypes/Traits/categories.yml @@ -0,0 +1,8 @@ +- type: traitCategory + id: Disabilities + name: trait-category-disabilities + +- type: traitCategory + id: SpeechTraits + name: trait-category-speech + maxTraitPoints: 2 diff --git a/Resources/Prototypes/Traits/disabilities.yml b/Resources/Prototypes/Traits/disabilities.yml index be1e981549..6e0026e44e 100644 --- a/Resources/Prototypes/Traits/disabilities.yml +++ b/Resources/Prototypes/Traits/disabilities.yml @@ -3,6 +3,7 @@ name: trait-blindness-name description: trait-blindness-desc traitGear: WhiteCane + category: Disabilities whitelist: components: - Blindable @@ -14,6 +15,7 @@ name: trait-poor-vision-name description: trait-poor-vision-desc traitGear: ClothingEyesGlasses + category: Disabilities whitelist: components: - Blindable @@ -25,6 +27,7 @@ id: Narcolepsy name: trait-narcolepsy-name description: trait-narcolepsy-desc + category: Disabilities components: - type: Narcolepsy timeBetweenIncidents: 300, 600 @@ -34,25 +37,15 @@ id: Pacifist name: trait-pacifist-name description: trait-pacifist-desc + category: Disabilities components: - type: Pacified -- type: trait - id: Paracusia - name: trait-paracusia-name - description: trait-paracusia-desc - components: - - type: Paracusia - minTimeBetweenIncidents: 0.1 - maxTimeBetweenIncidents: 300 - maxSoundDistance: 7 - sounds: - collection: Paracusia - - type: trait id: Unrevivable name: trait-unrevivable-name description: trait-unrevivable-desc + category: Disabilities components: - type: Unrevivable @@ -60,6 +53,7 @@ id: Muted name: trait-muted-name description: trait-muted-desc + category: Disabilities blacklist: components: - BorgChassis @@ -67,15 +61,31 @@ - type: Muted - type: trait - id: FrontalLisp - name: trait-frontal-lisp-name - description: trait-frontal-lisp-desc + id: LightweightDrunk + name: trait-lightweight-name + description: trait-lightweight-desc + category: Disabilities components: - - type: FrontalLisp + - type: LightweightDrunk + boozeStrengthMultiplier: 2 + +- type: trait + id: Paracusia + name: trait-paracusia-name + description: trait-paracusia-desc + category: Disabilities + components: + - type: Paracusia + minTimeBetweenIncidents: 0.1 + maxTimeBetweenIncidents: 300 + maxSoundDistance: 7 + sounds: + collection: Paracusia - type: trait id: Snoring name: trait-snoring-name description: trait-snoring-desc + category: Disabilities components: - type: Snoring diff --git a/Resources/Prototypes/Traits/inconveniences.yml b/Resources/Prototypes/Traits/inconveniences.yml deleted file mode 100644 index 657781d1b5..0000000000 --- a/Resources/Prototypes/Traits/inconveniences.yml +++ /dev/null @@ -1,18 +0,0 @@ -- type: trait - id: LightweightDrunk - name: trait-lightweight-name - description: trait-lightweight-desc - components: - - type: LightweightDrunk - boozeStrengthMultiplier: 2 - -- type: trait - id: SocialAnxiety - name: trait-socialanxiety-name - description: trait-socialanxiety-desc - components: - - type: StutteringAccent - matchRandomProb: 0.1 - fourRandomProb: 0 - threeRandomProb: 0 - cutRandomProb: 0 diff --git a/Resources/Prototypes/Traits/neutral.yml b/Resources/Prototypes/Traits/neutral.yml deleted file mode 100644 index 78d2bba049..0000000000 --- a/Resources/Prototypes/Traits/neutral.yml +++ /dev/null @@ -1,33 +0,0 @@ -- type: trait - id: PirateAccent - name: trait-pirate-accent-name - description: trait-pirate-accent-desc - components: - - type: PirateAccent - -- type: trait - id: Accentless - name: trait-accentless-name - description: trait-accentless-desc - components: - - type: Accentless - removes: - - type: LizardAccent - - type: MothAccent - - type: ReplacementAccent - accent: dwarf - -- type: trait - id: Southern - name: trait-southern-name - description: trait-southern-desc - components: - - type: SouthernAccent - -- type: trait - id: Liar - name: trait-liar-name - description: trait-liar-desc - components: - - type: ReplacementAccent - accent: liar diff --git a/Resources/Prototypes/Traits/speech.yml b/Resources/Prototypes/Traits/speech.yml new file mode 100644 index 0000000000..9448e160b5 --- /dev/null +++ b/Resources/Prototypes/Traits/speech.yml @@ -0,0 +1,88 @@ +# Free + +- type: trait + id: Accentless + name: trait-accentless-name + description: trait-accentless-desc + category: SpeechTraits + components: + - type: Accentless + removes: + - type: LizardAccent + - type: MothAccent + - type: ReplacementAccent + accent: dwarf + +# 1 Cost + +- type: trait + id: SouthernAccent + name: trait-southern-name + description: trait-southern-desc + category: SpeechTraits + cost: 1 + components: + - type: SouthernAccent + +- type: trait + id: PirateAccent + name: trait-pirate-accent-name + description: trait-pirate-accent-desc + category: SpeechTraits + cost: 1 + components: + - type: PirateAccent + +- type: trait + id: CowboyAccent + name: trait-cowboy-name + description: trait-cowboy-desc + category: SpeechTraits + cost: 1 + components: + - type: ReplacementAccent + accent: cowboy + +- type: trait + id: ItalianAccent + name: trait-italian-name + description: trait-italian-desc + category: SpeechTraits + cost: 1 + components: + - type: ReplacementAccent + accent: italian + +- type: trait + id: Liar + name: trait-liar-name + description: trait-liar-desc + category: SpeechTraits + cost: 1 + components: + - type: ReplacementAccent + accent: liar + +# 2 Cost + +- type: trait + id: SocialAnxiety + name: trait-socialanxiety-name + description: trait-socialanxiety-desc + category: SpeechTraits + cost: 2 + 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: 2 + components: + - type: FrontalLisp -- 2.51.2