diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index 952c9ff91e2..4333ac5112d 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -565,7 +565,7 @@ namespace Content.Client.Lobby.UI } Traits.SetSelectedTraits(selectedTraits); - Traits.UpdateConditions(Profile.Species); + Traits.UpdateConditions(Profile); } // End DeltaV - Traits Integration diff --git a/Content.Client/_DV/Traits/UI/TraitCategory.xaml.cs b/Content.Client/_DV/Traits/UI/TraitCategory.xaml.cs index 69654424e3a..ae377ce0e17 100644 --- a/Content.Client/_DV/Traits/UI/TraitCategory.xaml.cs +++ b/Content.Client/_DV/Traits/UI/TraitCategory.xaml.cs @@ -171,11 +171,11 @@ public sealed partial class TraitCategory : BoxContainer /// Updates condition states for all trait entries based on current job/species. /// Traits that don't meet conditions are disabled but still visible. /// - public void UpdateConditions(ProtoId? jobId, ProtoId? speciesId) + public void UpdateConditions(ProtoId? jobId, ProtoId? speciesId, IReadOnlySet>? antagPreferences) { foreach (var (_, entry) in _traitEntries) { - entry.UpdateConditionsMet(jobId, speciesId); + entry.UpdateConditionsMet(jobId, speciesId, antagPreferences); } // Update stats since some traits may have been deselected diff --git a/Content.Client/_DV/Traits/UI/TraitEntry.xaml.cs b/Content.Client/_DV/Traits/UI/TraitEntry.xaml.cs index 9aafc5fe71c..80b926f8958 100644 --- a/Content.Client/_DV/Traits/UI/TraitEntry.xaml.cs +++ b/Content.Client/_DV/Traits/UI/TraitEntry.xaml.cs @@ -91,7 +91,7 @@ public sealed partial class TraitEntry : PanelContainer /// /// Updates whether conditions are met based on current job/species. /// - public void UpdateConditionsMet(ProtoId? jobId, ProtoId? speciesId) + public void UpdateConditionsMet(ProtoId? jobId, ProtoId? speciesId, IReadOnlySet>? antagPreferences) { _failedConditionTooltips.Clear(); MeetsConditions = true; @@ -104,7 +104,8 @@ public sealed partial class TraitEntry : PanelContainer HasJobCondition jobCond => CheckJobCondition(jobCond, jobId), InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId), HasCompCondition compCond => !compCond.Invert, // can't check in lobby but screws with the inversion logic - AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId), + IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences), + AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences), _ => true, }; @@ -150,7 +151,15 @@ public sealed partial class TraitEntry : PanelContainer return department.Roles.Contains(job); } - private bool CheckAnyOfCondition(AnyOfCondition condition, ProtoId? jobId, ProtoId? speciesId) + private bool CheckAntagEligibleCondition(IsAntagEligibleCondition condition, IReadOnlySet>? antagPreferences) + { + if (antagPreferences is null) + return false; + + return antagPreferences.Contains(condition.Antag); + } + + private bool CheckAnyOfCondition(AnyOfCondition condition, ProtoId? jobId, ProtoId? speciesId, IReadOnlySet>? antagPreferences) { if (condition.Conditions.Count == 0) return false; @@ -164,7 +173,8 @@ public sealed partial class TraitEntry : PanelContainer HasJobCondition jobCond => CheckJobCondition(jobCond, jobId), InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId), HasCompCondition compCond => !compCond.Invert, // can't check in lobby - AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId), // Recursive! + AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences), // Recursive! + IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences), _ => true, }; diff --git a/Content.Client/_DV/Traits/UI/TraitsTab.xaml.cs b/Content.Client/_DV/Traits/UI/TraitsTab.xaml.cs index b47e75d04f5..d246992e24f 100644 --- a/Content.Client/_DV/Traits/UI/TraitsTab.xaml.cs +++ b/Content.Client/_DV/Traits/UI/TraitsTab.xaml.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Shared._DV.CCVars; using Content.Shared._DV.Traits; using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Preferences; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; @@ -32,7 +33,7 @@ public sealed partial class TraitsTab : BoxContainer private string _currentSearchText = string.Empty; private bool _awaitingLayoutUpdate; - private ProtoId? _currentSpeciesId; + private HumanoidCharacterProfile? _profile; public TraitsTab() { @@ -268,10 +269,10 @@ public sealed partial class TraitsTab : BoxContainer /// Updates all trait conditions based on current job and species. /// Call this when job or species changes in the character creator. /// - /// Current species ID, or null if none selected - public void UpdateConditions(ProtoId speciesId) + /// Current profile + public void UpdateConditions(HumanoidCharacterProfile profile) { - _currentSpeciesId = speciesId; + _profile = profile; UpdateAllConditions(); } @@ -280,7 +281,7 @@ public sealed partial class TraitsTab : BoxContainer foreach (var (_, categoryUi) in _categoryUis) { // If some fork wants to use the top selected job as well, just add that to the UpdateConditions method in the editor - categoryUi.UpdateConditions(null, _currentSpeciesId); + categoryUi.UpdateConditions(null, _profile?.Species, _profile?.AntagPreferences); } RecalculateStats(); diff --git a/Content.IntegrationTests/Tests/_DV/TraitSystemTest.cs b/Content.IntegrationTests/Tests/_DV/TraitSystemTest.cs index 2f181086758..5daf838b25f 100644 --- a/Content.IntegrationTests/Tests/_DV/TraitSystemTest.cs +++ b/Content.IntegrationTests/Tests/_DV/TraitSystemTest.cs @@ -598,7 +598,7 @@ public sealed partial class TraitSystemTest BindingFlags.NonPublic | BindingFlags.Instance); var validTraits = (HashSet>)method?.Invoke(traitSys, - new object[] { player, selectedTraits, null, null, null, new Dictionary, List>() }); + new object[] { player, selectedTraits, null, null, null, null, new Dictionary, List>() }); Assert.Multiple(() => { @@ -639,7 +639,7 @@ public sealed partial class TraitSystemTest BindingFlags.NonPublic | BindingFlags.Instance); var validTraits = (HashSet>)method?.Invoke(traitSys, - new object[] { player, selectedTraits, null, null, null, new Dictionary, List>()}); + new object[] { player, selectedTraits, null, null, null, null, new Dictionary, List>()}); Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxTraits limit"); @@ -673,7 +673,7 @@ public sealed partial class TraitSystemTest BindingFlags.NonPublic | BindingFlags.Instance); var validTraits = (HashSet>)method?.Invoke(traitSys, - new object[] { player, selectedTraits, null, null, null, new Dictionary, List>() }); + new object[] { player, selectedTraits, null, null, null, null, new Dictionary, List>() }); Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxPoints limit"); @@ -706,7 +706,7 @@ public sealed partial class TraitSystemTest BindingFlags.NonPublic | BindingFlags.Instance); var validTraits = (HashSet>)method?.Invoke(traitSys, - new object[] { player, selectedTraits, null, null, null, new Dictionary, List>() }); + new object[] { player, selectedTraits, null, null, null, null, new Dictionary, List>() }); Assert.That(validTraits?.Contains("TestTraitHasComp"), Is.True, "Trait with met condition should be valid"); @@ -739,7 +739,7 @@ public sealed partial class TraitSystemTest BindingFlags.NonPublic | BindingFlags.Instance); var validTraits = (HashSet>)method?.Invoke(traitSys, - new object[] { player, selectedTraits, null, null, null, new Dictionary, List>() }); + new object[] { player, selectedTraits, null, null, null, null, new Dictionary, List>() }); Assert.That(validTraits?.Contains("TestTraitHasComp"), Is.False, diff --git a/Content.Server/_DV/Traits/TraitSystem.cs b/Content.Server/_DV/Traits/TraitSystem.cs index 1ee7fac71da..f3d67e2571c 100644 --- a/Content.Server/_DV/Traits/TraitSystem.cs +++ b/Content.Server/_DV/Traits/TraitSystem.cs @@ -6,6 +6,8 @@ using Content.Shared.GameTicking; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Preferences; using Content.Shared.Roles; using Robust.Shared.Configuration; using Robust.Shared.Player; @@ -45,8 +47,8 @@ public sealed class TraitSystem : EntitySystem !jobProto.ApplyTraits) return; - // Get species ID for condition checking - string? speciesId = null; + // Use the species ID from the profile if for some reason we can't get the humanoid appearance + ProtoId? speciesId = args.Profile.Species; if (TryComp(args.Mob, out var humanoid)) speciesId = humanoid.Species; @@ -54,7 +56,7 @@ public sealed class TraitSystem : EntitySystem var disabledTraits = new Dictionary, List>(); // Validate and collect valid traits - var validTraits = ValidateTraits(args.Mob, args.Profile.TraitPreferences, args.Player, args.JobId, speciesId, disabledTraits); + var validTraits = ValidateTraits(args.Mob, args.Profile.TraitPreferences, args.Player, args.JobId, speciesId, args.Profile, disabledTraits); // Apply valid traits foreach (var traitId in validTraits) @@ -81,6 +83,7 @@ public sealed class TraitSystem : EntitySystem ICommonSession? session, string? jobId, string? speciesId, + HumanoidCharacterProfile? profile, Dictionary, List> disabledTraits) { var validTraits = new HashSet>(); @@ -100,6 +103,7 @@ public sealed class TraitSystem : EntitySystem LogMan = _log, JobId = jobId, SpeciesId = speciesId, + Profile = profile, }; foreach (var traitId in selectedTraits) diff --git a/Content.Shared/_DV/Traits/Conditions/BaseTraitCondition.cs b/Content.Shared/_DV/Traits/Conditions/BaseTraitCondition.cs index eeb9284168d..293dceeb7ec 100644 --- a/Content.Shared/_DV/Traits/Conditions/BaseTraitCondition.cs +++ b/Content.Shared/_DV/Traits/Conditions/BaseTraitCondition.cs @@ -1,3 +1,4 @@ +using Content.Shared.Preferences; using JetBrains.Annotations; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -57,4 +58,9 @@ public sealed class TraitConditionContext /// The species ID of the player, if available. /// public string? SpeciesId { get; init; } + + /// + /// The of the player, if available. + /// + public HumanoidCharacterProfile? Profile { get; init; } } diff --git a/Content.Shared/_DV/Traits/Conditions/IsAntagEligibleCondition.cs b/Content.Shared/_DV/Traits/Conditions/IsAntagEligibleCondition.cs new file mode 100644 index 00000000000..7cb13ae6b32 --- /dev/null +++ b/Content.Shared/_DV/Traits/Conditions/IsAntagEligibleCondition.cs @@ -0,0 +1,37 @@ +using Content.Shared.Roles; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DV.Traits.Conditions; + +/// +/// Condition that checks if the player has enabled a specific antag preference. +/// +public sealed partial class IsAntagEligibleCondition : BaseTraitCondition +{ + /// + /// The antag prototype ID to check for eligibility. + /// + [DataField(required: true)] + public ProtoId Antag; + + protected override bool EvaluateImplementation(TraitConditionContext ctx) + { + if (ctx.Profile == null) + return false; + + // Check if the player has this antag preference enabled + return ctx.Profile.AntagPreferences.Contains(Antag); + } + + public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc) + { + if (!proto.TryIndex(Antag, out var antagProto)) + return string.Empty; + + var antagName = loc.GetString(antagProto.Name); + + return Invert + ? loc.GetString("trait-condition-antag-not", ("antag", antagName)) + : loc.GetString("trait-condition-antag-is", ("antag", antagName)); + } +} diff --git a/Resources/Locale/en-US/_DV/traits/trait-categories.ftl b/Resources/Locale/en-US/_DV/traits/trait-categories.ftl index 2bf7599b466..fef3f7debe8 100644 --- a/Resources/Locale/en-US/_DV/traits/trait-categories.ftl +++ b/Resources/Locale/en-US/_DV/traits/trait-categories.ftl @@ -2,3 +2,4 @@ trait-category-disabilities = Disabilities trait-category-medical = Medical trait-category-mental = Mental trait-category-accents = Accents +trait-category-meta = Meta diff --git a/Resources/Locale/en-US/_DV/traits/traits-ui.ftl b/Resources/Locale/en-US/_DV/traits/traits-ui.ftl index 4f1e326a8b2..b5255e7b377 100644 --- a/Resources/Locale/en-US/_DV/traits/traits-ui.ftl +++ b/Resources/Locale/en-US/_DV/traits/traits-ui.ftl @@ -50,3 +50,7 @@ trait-condition-department-not = You must not be in the [color={$color}]{$depart ## HasComp borg conditions trait-condition-borg-not = You must not be a [color=yellow]borg[/color]. + +# Antag conditions +trait-condition-antag-is = Must be eligible for [color=red]{$antag}[/color] antag role. +trait-condition-antag-not = Must not be eligible for [color=red]{$antag}[/color] antag role. diff --git a/Resources/Locale/en-US/_DV/traits/traits.ftl b/Resources/Locale/en-US/_DV/traits/traits.ftl index 08f59ea975f..c568c3fb431 100644 --- a/Resources/Locale/en-US/_DV/traits/traits.ftl +++ b/Resources/Locale/en-US/_DV/traits/traits.ftl @@ -64,3 +64,6 @@ trait-redshirt-desc = trait-anglish-name = Anglish tung trait-anglish-desc = Your tung is of the Saxons, and say unlike others aboard this skyship. + +trait-protected-name = Marked as Protected +trait-protected-desc = Due to your position, value, or circumstances, your survival is considered preferable. You won't be targeted by kill objectives. diff --git a/Resources/Prototypes/_DV/Traits/meta.yml b/Resources/Prototypes/_DV/Traits/meta.yml new file mode 100644 index 00000000000..680ec41d852 --- /dev/null +++ b/Resources/Prototypes/_DV/Traits/meta.yml @@ -0,0 +1,25 @@ +- type: trait + id: MarkedAsProtected + name: trait-protected-name + description: trait-protected-desc + category: Meta + conditions: + - !type:InDepartmentCondition + department: Command + invert: true + - !type:InDepartmentCondition + department: Security + invert: true + - !type:IsAntagEligibleCondition + antag: Thief + invert: true + - !type:IsAntagEligibleCondition + antag: Traitor + invert: true + - !type:IsAntagEligibleCondition + antag: TraitorSleeper + invert: true + effects: + - !type:AddCompsEffect + components: + - type: TargetObjectiveImmune diff --git a/Resources/Prototypes/_DV/Traits/trait_categories.yml b/Resources/Prototypes/_DV/Traits/trait_categories.yml index df4628504a8..b25db2cc894 100644 --- a/Resources/Prototypes/_DV/Traits/trait_categories.yml +++ b/Resources/Prototypes/_DV/Traits/trait_categories.yml @@ -23,3 +23,10 @@ name: trait-category-medical priority: 40 accentColor: "#ef4444" # Red + +- type: traitCategory + id: Meta + name: trait-category-meta + priority: 50 + maxTraits: 1 + accentColor: "#eab308" # Yellow