Compare commits

...
3 Commits
Author SHA1 Message Date
Tobias Berger 66f23833c6 Fix tests 2026-01-30 04:08:49 +01:00
Tobias Berger 333948591f More trait stuff
HasTraitCondition instead of Conflicts and Requirements
RemoveBodyPartEffect instead of AmputeeComponent/System
Remove redundant BorgChassis checks
Remove failed condition check and just show the full one
Improve vision trait conditions
2026-01-30 03:12:03 +01:00
Milon 51d605f67b Formatting and some other fixes 2026-01-30 00:35:09 +01:00
23 changed files with 281 additions and 258 deletions
@@ -70,8 +70,7 @@ public sealed partial class DisabledTraitsPopup : FancyWindow
{ {
Margin = new Thickness(10, 2, 0, 0) Margin = new Thickness(10, 2, 0, 0)
}; };
reasonLabel.SetMessage(FormattedMessage.FromMarkupOrThrow( reasonLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(reason));
Loc.GetString("disabled-traits-popup-reason", ("reason", reason))));
container.AddChild(reasonLabel); container.AddChild(reasonLabel);
} }
@@ -171,11 +171,11 @@ public sealed partial class TraitCategory : BoxContainer
/// Updates condition states for all trait entries based on current job/species. /// Updates condition states for all trait entries based on current job/species.
/// Traits that don't meet conditions are disabled but still visible. /// Traits that don't meet conditions are disabled but still visible.
/// </summary> /// </summary>
public void UpdateConditions(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences) public void UpdateConditions(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
{ {
foreach (var (_, entry) in _traitEntries) foreach (var (_, entry) in _traitEntries)
{ {
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences); entry.UpdateConditionsMet(jobId, speciesId, antagPreferences, traits);
} }
// Update stats since some traits may have been deselected // Update stats since some traits may have been deselected
+23 -27
View File
@@ -1,3 +1,4 @@
using System.Text;
using Content.Shared._DV.Traits; using Content.Shared._DV.Traits;
using Content.Shared._DV.Traits.Conditions; using Content.Shared._DV.Traits.Conditions;
using Content.Shared.Humanoid.Prototypes; using Content.Shared.Humanoid.Prototypes;
@@ -25,7 +26,6 @@ public sealed partial class TraitEntry : PanelContainer
private readonly TraitPrototype _trait; private readonly TraitPrototype _trait;
private bool _isUpdating; private bool _isUpdating;
private readonly List<string> _failedConditionTooltips = new();
public TraitEntry(TraitPrototype trait) public TraitEntry(TraitPrototype trait)
{ {
@@ -55,24 +55,23 @@ public sealed partial class TraitEntry : PanelContainer
private void UpdateConditionTooltips() private void UpdateConditionTooltips()
{ {
var tooltips = new List<string>(); var conditionsTooltip = new StringBuilder();
foreach (var condition in _trait.Conditions) foreach (var condition in _trait.Conditions)
{ {
var tooltip = condition.GetTooltip(_prototype, _loc); conditionsTooltip.Append(condition.GetTooltip(_prototype, _loc, 0));
if (!string.IsNullOrEmpty(tooltip))
tooltips.Add(tooltip);
} }
if (tooltips.Count > 0) if (conditionsTooltip.Length > 0)
{ {
var tooltipText = Loc.GetString("trait-conditions-tooltip", var tooltip = Loc.GetString("trait-conditions-tooltip", ("conditions", conditionsTooltip.ToString())).ToString().Trim();
("requirements", string.Join("\n", tooltips))); var markupTooltip = CreateMarkupTooltip(tooltip);
TooltipSupplier = _ => markupTooltip;
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
} }
else else
{
TooltipSupplier = null; TooltipSupplier = null;
}
} }
/// <summary> /// <summary>
@@ -91,9 +90,8 @@ public sealed partial class TraitEntry : PanelContainer
/// <summary> /// <summary>
/// Updates whether conditions are met based on current job/species. /// Updates whether conditions are met based on current job/species.
/// </summary> /// </summary>
public void UpdateConditionsMet(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences) public void UpdateConditionsMet(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
{ {
_failedConditionTooltips.Clear();
MeetsConditions = true; MeetsConditions = true;
foreach (var condition in _trait.Conditions) foreach (var condition in _trait.Conditions)
@@ -105,7 +103,8 @@ public sealed partial class TraitEntry : PanelContainer
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId), InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby but screws with the inversion logic HasCompCondition compCond => !compCond.Invert, // can't check in lobby but screws with the inversion logic
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences), IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences), HasTraitCondition hasTrait => CheckHasTraitCondition(hasTrait, traits),
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences, traits),
_ => true, _ => true,
}; };
@@ -115,9 +114,6 @@ public sealed partial class TraitEntry : PanelContainer
if (!result) if (!result)
{ {
MeetsConditions = false; MeetsConditions = false;
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
_failedConditionTooltips.Add(tooltip);
} }
} }
@@ -159,7 +155,15 @@ public sealed partial class TraitEntry : PanelContainer
return antagPreferences.Contains(condition.Antag); return antagPreferences.Contains(condition.Antag);
} }
private bool CheckAnyOfCondition(AnyOfCondition condition, ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences) private bool CheckHasTraitCondition(HasTraitCondition condition, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
{
if (traits is null)
return false;
return traits.Contains(condition.Trait);
}
private bool CheckAnyOfCondition(AnyOfCondition condition, ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
{ {
if (condition.Conditions.Count == 0) if (condition.Conditions.Count == 0)
return false; return false;
@@ -173,8 +177,9 @@ public sealed partial class TraitEntry : PanelContainer
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId), HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId), InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby HasCompCondition compCond => !compCond.Invert, // can't check in lobby
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences), // Recursive!
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences), IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
HasTraitCondition hasTrait => CheckHasTraitCondition(hasTrait, traits),
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences, traits), // Recursive!
_ => true, _ => true,
}; };
@@ -210,15 +215,6 @@ public sealed partial class TraitEntry : PanelContainer
// Add disabled styling // Add disabled styling
AddStyleClass("TraitsEntryDisabled"); 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 else
{ {
+3 -13
View File
@@ -1,7 +1,6 @@
using System.Linq; using System.Linq;
using Content.Shared._DV.CCVars; using Content.Shared._DV.CCVars;
using Content.Shared._DV.Traits; using Content.Shared._DV.Traits;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
@@ -154,16 +153,6 @@ public sealed partial class TraitsTab : BoxContainer
} }
} }
// Check conflicts
foreach (var conflict in trait.Conflicts)
{
if (!_selectedTraits.Contains(conflict))
continue;
RevertTraitToggle(traitId);
return;
}
_selectedTraits.Add(traitId); _selectedTraits.Add(traitId);
_currentTraitCount++; _currentTraitCount++;
_currentPointsSpent += trait.Cost; _currentPointsSpent += trait.Cost;
@@ -177,6 +166,7 @@ public sealed partial class TraitsTab : BoxContainer
UpdateGlobalStats(); UpdateGlobalStats();
UpdateCategoryStats(trait.Category); UpdateCategoryStats(trait.Category);
UpdateAllConditions();
OnTraitsChanged?.Invoke(_selectedTraits); OnTraitsChanged?.Invoke(_selectedTraits);
} }
@@ -211,7 +201,7 @@ public sealed partial class TraitsTab : BoxContainer
// If parent width is 0 (not laid out yet), defer until layout happens // If parent width is 0 (not laid out yet), defer until layout happens
if (parentWidth > 0) if (parentWidth > 0)
{ {
GlobalPointsBar.SetWidth = (int)((parentWidth - 2 ) * percentage); GlobalPointsBar.SetWidth = (int)((parentWidth - 2) * percentage);
_awaitingLayoutUpdate = false; _awaitingLayoutUpdate = false;
} }
else if (!_awaitingLayoutUpdate) else if (!_awaitingLayoutUpdate)
@@ -281,7 +271,7 @@ public sealed partial class TraitsTab : BoxContainer
foreach (var (_, categoryUi) in _categoryUis) 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 // 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, _profile?.Species, _profile?.AntagPreferences); categoryUi.UpdateConditions(null, _profile?.Species, _profile?.AntagPreferences, _selectedTraits);
} }
RecalculateStats(); RecalculateStats();
@@ -6,7 +6,10 @@ using Content.Shared._DV.Traits.Conditions;
using Content.Shared._DV.Traits.Effects; using Content.Shared._DV.Traits.Effects;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
@@ -20,7 +23,7 @@ namespace Content.IntegrationTests.Tests._DV;
/// Tests all conditions, effects, and validation logic. /// Tests all conditions, effects, and validation logic.
/// </summary> /// </summary>
[TestFixture] [TestFixture]
[TestOf(typeof(TraitSystemTest))] [TestOf(typeof(TraitSystem))]
public sealed partial class TraitSystemTest public sealed partial class TraitSystemTest
{ {
[TestPrototypes] [TestPrototypes]
@@ -107,8 +110,10 @@ public sealed partial class TraitSystemTest
description: trait-dysgraphia-name description: trait-dysgraphia-name
category: TestCategoryUnlimited category: TestCategoryUnlimited
cost: 0 cost: 0
conflicts: conditions:
- TestTraitConflictB - !type:HasTraitCondition
trait: TestTraitConflictB
invert: true
effects: effects:
- !type:AddCompsEffect - !type:AddCompsEffect
components: components:
@@ -582,31 +587,29 @@ public sealed partial class TraitSystemTest
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true }); await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server; var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>(); var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ProtoMan;
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace); var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var selectedTraits = new HashSet<ProtoId<TraitPrototype>> var profile = new HumanoidCharacterProfile()
{ .WithTraitPreference("TestTraitConflictA", protoMan)
"TestTraitConflictA", .WithTraitPreference("TestTraitConflictB", protoMan);
"TestTraitConflictB",
};
var traitSys = entMan.System<TraitSystem>(); var traitSys = entMan.System<TraitSystem>();
var method = typeof(TraitSystem).GetMethod("ValidateTraits", var method = typeof(TraitSystem).GetMethod("ValidateTraits",
BindingFlags.NonPublic | BindingFlags.Instance); BindingFlags.NonPublic | BindingFlags.Instance);
var disabledTraits = new Dictionary<ProtoId<TraitPrototype>, List<string>>();
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys, var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() }); new object[] { player, profile.TraitPreferences, null, null, null, profile, disabledTraits });
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(validTraits?.Count, Is.EqualTo(1), "Only one conflicting trait should be valid"); 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.True, "Non-complaining trait should be kept");
Assert.That(validTraits.Contains("TestTraitConflictB"), Assert.That(disabledTraits.ContainsKey("TestTraitConflictA"), Is.True, "Trait with conflicts should be rejected");
Is.False,
"Conflicting trait should be rejected");
}); });
entMan.DeleteEntity(player); entMan.DeleteEntity(player);
@@ -639,7 +642,7 @@ public sealed partial class TraitSystemTest
BindingFlags.NonPublic | BindingFlags.Instance); BindingFlags.NonPublic | BindingFlags.Instance);
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys, var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>()}); new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxTraits limit"); Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxTraits limit");
@@ -669,6 +672,7 @@ public sealed partial class TraitSystemTest
}; };
var traitSys = entMan.System<TraitSystem>(); var traitSys = entMan.System<TraitSystem>();
var method = typeof(TraitSystem).GetMethod("ValidateTraits", var method = typeof(TraitSystem).GetMethod("ValidateTraits",
BindingFlags.NonPublic | BindingFlags.Instance); BindingFlags.NonPublic | BindingFlags.Instance);
@@ -760,8 +764,8 @@ public sealed partial class TraitSystemTest
IPrototypeManager protoMan, IPrototypeManager protoMan,
IComponentFactory factory, IComponentFactory factory,
EntityUid player, EntityUid player,
string? jobId = null, ProtoId<JobPrototype>? jobId = null,
string? speciesId = null) ProtoId<SpeciesPrototype>? speciesId = null)
{ {
return new TraitConditionContext return new TraitConditionContext
{ {
@@ -1,23 +0,0 @@
using Content.Shared.Body.Part;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Traits.Assorted;
[RegisterComponent]
public sealed partial class AmputeeComponent : Component
{
[DataField]
public BodyPartType RemoveBodyPart { get; private set; } = BodyPartType.Arm;
[DataField]
public BodyPartSymmetry PartSymmetry { get; private set; } = BodyPartSymmetry.Left;
/// <summary>
/// Body part prototype to use as a replacement limb, if applicable.
/// </summary>
[DataField]
public EntProtoId? ProtoId { get; private set; }
[DataField]
public string? SlotId { get; private set; } = "left arm";
}
@@ -1,57 +0,0 @@
using Content.Server.Body.Systems;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Robust.Server.GameObjects;
namespace Content.Server._DV.Traits.Assorted;
public sealed class AmputeeSystem : EntitySystem
{
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AmputeeComponent, MapInitEvent>(OnMapInit);
}
// Logic here taken from Den at https://github.com/TheDenSS14/TheDen/blob/d6f85a10fccf0f282981438e55b808d7ece73ad9/Content.Server/Traits/TraitSystem.Functions.cs
private void OnMapInit(Entity<AmputeeComponent> ent, ref MapInitEvent args)
{
if (!TryComp(ent, out BodyComponent? body) || !TryComp(ent, out TransformComponent? xform))
return;
var root = _body.GetRootPartOrNull(ent, body);
if (root is null)
return;
var parts = _body.GetBodyChildrenOfType(ent, ent.Comp.RemoveBodyPart, body);
foreach (var part in parts)
{
var partComp = part.Component;
if (partComp.Symmetry != ent.Comp.PartSymmetry)
continue;
foreach (var child in _body.GetBodyPartChildren(part.Id, part.Component))
{
QueueDel(child.Id);
}
_transform.AttachToGridOrMap(part.Id);
QueueDel(part.Id);
// apparently chopping off limbs makes people bleed a lot. Who would have guessed?
_bloodstream.TryModifyBleedAmount(ent.Owner, -10f);
// goes unused for the purposes of the arm amputee traits, but might as well keep it in
if (ent.Comp.ProtoId is null || ent.Comp.SlotId == null)
continue;
var newLimb = SpawnAtPosition(ent.Comp.ProtoId, xform.Coordinates);
if (TryComp<BodyPartComponent>(newLimb, out var limbComp) && limbComp.Symmetry == ent.Comp.PartSymmetry)
_body.AttachPart(root.Value.Entity, ent.Comp.SlotId, newLimb, root.Value.BodyPart, limbComp);
}
}
}
+3 -38
View File
@@ -81,8 +81,8 @@ public sealed class TraitSystem : EntitySystem
EntityUid player, EntityUid player,
IReadOnlySet<ProtoId<TraitPrototype>> selectedTraits, IReadOnlySet<ProtoId<TraitPrototype>> selectedTraits,
ICommonSession? session, ICommonSession? session,
string? jobId, ProtoId<JobPrototype>? jobId,
string? speciesId, ProtoId<SpeciesPrototype>? speciesId,
HumanoidCharacterProfile? profile, HumanoidCharacterProfile? profile,
Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits) Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
{ {
@@ -142,41 +142,6 @@ public sealed class TraitSystem : EntitySystem
continue; 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}");
if (_prototype.TryIndex(validTraitId, out var conflictTrait))
{
rejectionReasons.Add(Loc.GetString("disabled-traits-reason-conflict",
("trait", Loc.GetString(conflictTrait.Name))));
}
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");
rejectionReasons.Add(Loc.GetString("disabled-traits-reason-conflict",
("trait", Loc.GetString(validTrait.Name))));
hasConflict = true;
break;
}
}
if (hasConflict)
{
disabledTraits[traitId] = rejectionReasons;
continue;
}
// Check all conditions // Check all conditions
if (!CheckConditions(trait, conditionCtx, rejectionReasons)) if (!CheckConditions(trait, conditionCtx, rejectionReasons))
{ {
@@ -246,7 +211,7 @@ public sealed class TraitSystem : EntitySystem
continue; continue;
// Get human-readable reason from the condition // Get human-readable reason from the condition
var tooltip = condition.GetTooltip(ctx.Proto, Loc); var tooltip = condition.GetTooltip(ctx.Proto, Loc, 0);
if (!string.IsNullOrEmpty(tooltip)) if (!string.IsNullOrEmpty(tooltip))
rejectionReasons.Add(tooltip); rejectionReasons.Add(tooltip);
@@ -1,3 +1,4 @@
using System.Text;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared._DV.Traits.Conditions; namespace Content.Shared._DV.Traits.Conditions;
@@ -38,27 +39,25 @@ public sealed partial class AnyOfCondition : BaseTraitCondition
return false; return false;
} }
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc) public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc, int depth)
{ {
if (Conditions.Count == 0) if (Conditions.Count == 0)
return string.Empty; return string.Empty;
var requirements = new List<string>(); var requirementsTooltip = new StringBuilder();
foreach (var condition in Conditions) foreach (var condition in Conditions)
{ {
var tooltip = condition.GetTooltip(proto, loc); var conditionTooltip = condition.GetTooltip(proto, loc, depth + 1);
if (!string.IsNullOrEmpty(tooltip)) if (conditionTooltip.Length > 0)
requirements.Add(tooltip); requirementsTooltip.Append(conditionTooltip);
} }
if (requirements.Count == 0) if (requirementsTooltip.Length == 0)
return string.Empty; return string.Empty;
// AAAAAAAAAAA var tooltip = loc.GetString("trait-condition-any-of", ("requirements", requirementsTooltip));
var joinedRequirements = string.Join("\n• ", requirements);
// Handle inversion in tooltip (ANY becomes NONE when inverted) return new string(' ', depth * 2) + "- " + tooltip;
return Loc.GetString("trait-condition-any-of", ("requirements", joinedRequirements));
} }
} }
@@ -1,4 +1,6 @@
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Roles;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -31,7 +33,7 @@ public abstract partial class BaseTraitCondition
/// Generates a human-readable tooltip describing this condition's requirements. /// Generates a human-readable tooltip describing this condition's requirements.
/// </summary> /// </summary>
[PublicAPI] [PublicAPI]
public abstract string GetTooltip(IPrototypeManager proto, ILocalizationManager loc); public abstract string GetTooltip(IPrototypeManager proto, ILocalizationManager loc, int depth);
protected abstract bool EvaluateImplementation(TraitConditionContext ctx); protected abstract bool EvaluateImplementation(TraitConditionContext ctx);
} }
@@ -52,12 +54,12 @@ public sealed class TraitConditionContext
/// <summary> /// <summary>
/// The job ID of the player, if available. /// The job ID of the player, if available.
/// </summary> /// </summary>
public string? JobId { get; init; } public ProtoId<JobPrototype>? JobId { get; init; }
/// <summary> /// <summary>
/// The species ID of the player, if available. /// The species ID of the player, if available.
/// </summary> /// </summary>
public string? SpeciesId { get; init; } public ProtoId<SpeciesPrototype>? SpeciesId { get; init; }
/// <summary> /// <summary>
/// The <see cref="HumanoidCharacterProfile"/> of the player, if available. /// The <see cref="HumanoidCharacterProfile"/> of the player, if available.
@@ -39,11 +39,11 @@ public sealed partial class HasCompCondition : BaseTraitCondition
} }
} }
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc) public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc, int depth)
{ {
// If there's a custom tooltip supplied, use that // If there's a custom tooltip supplied, use that
if (Tooltip is not null) if (Tooltip is not null)
return Loc.GetString(Tooltip); return new string(' ', depth * 2) + "- " + loc.GetString(Tooltip) + Environment.NewLine;
// No tooltip for this condition since we're dealing with comps // No tooltip for this condition since we're dealing with comps
return string.Empty; return string.Empty;
@@ -17,13 +17,13 @@ public sealed partial class HasJobCondition : BaseTraitCondition
protected override bool EvaluateImplementation(TraitConditionContext ctx) protected override bool EvaluateImplementation(TraitConditionContext ctx)
{ {
if (string.IsNullOrEmpty(ctx.JobId)) if (ctx.JobId is not { } jobId)
return false; return false;
return ctx.JobId == Job; return jobId == Job;
} }
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc) public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc, int depth)
{ {
var jobName = Job.Id; var jobName = Job.Id;
var jobColor = "#ffffff"; var jobColor = "#ffffff";
@@ -43,8 +43,10 @@ public sealed partial class HasJobCondition : BaseTraitCondition
} }
} }
return Invert var tooltip = Invert
? loc.GetString("trait-condition-job-not", ("job", jobName), ("color", jobColor)) ? loc.GetString("trait-condition-job-not", ("job", jobName), ("color", jobColor))
: loc.GetString("trait-condition-job-is", ("job", jobName), ("color", jobColor)); : loc.GetString("trait-condition-job-is", ("job", jobName), ("color", jobColor));
return new string(' ', depth * 2) + "- " + tooltip + Environment.NewLine;
} }
} }
@@ -0,0 +1,36 @@
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.Traits.Conditions;
/// <summary>
/// Condition that checks if the player has activated a specific trait.
/// </summary>
public sealed partial class HasTraitCondition : BaseTraitCondition
{
/// <summary>
/// List of conditions to check. Passes if any condition evaluates to true.
/// </summary>
[DataField(required: true)]
public ProtoId<TraitPrototype> Trait = default!;
protected override bool EvaluateImplementation(TraitConditionContext ctx)
{
if (ctx.Profile is not { } profile)
return false;
return profile
.GetValidTraits(ctx.Profile.TraitPreferences, ctx.Proto)
.Contains(Trait);
}
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc, int depth)
{
if (!proto.TryIndex(Trait, out var trait))
return string.Empty;
var tooltip = Invert
? loc.GetString("trait-condition-trait-has-not", ("trait", loc.GetString(trait.Name)))
: loc.GetString("trait-condition-trait-has", ("trait", loc.GetString(trait.Name)));
return new string(' ', depth * 2) + "- " + tooltip + Environment.NewLine;
}
}
@@ -17,16 +17,16 @@ public sealed partial class InDepartmentCondition : BaseTraitCondition
protected override bool EvaluateImplementation(TraitConditionContext ctx) protected override bool EvaluateImplementation(TraitConditionContext ctx)
{ {
if (string.IsNullOrEmpty(ctx.JobId)) if (ctx.JobId is not { } jobId)
return false; return false;
if (!ctx.Proto.TryIndex(Department, out var department)) if (!ctx.Proto.TryIndex(Department, out var department))
return false; return false;
return department.Roles.Contains(ctx.JobId); return department.Roles.Contains(jobId);
} }
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc) public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc, int depth)
{ {
var deptName = Department.Id; var deptName = Department.Id;
var deptColor = "#ffffff"; var deptColor = "#ffffff";
@@ -37,9 +37,10 @@ public sealed partial class InDepartmentCondition : BaseTraitCondition
deptColor = deptProto.Color.ToHex(); deptColor = deptProto.Color.ToHex();
} }
var tooltip = Invert
return Invert
? loc.GetString("trait-condition-department-not", ("department", deptName), ("color", deptColor)) ? loc.GetString("trait-condition-department-not", ("department", deptName), ("color", deptColor))
: loc.GetString("trait-condition-department-is", ("department", deptName), ("color", deptColor)); : loc.GetString("trait-condition-department-is", ("department", deptName), ("color", deptColor));
return new string(' ', depth * 2) + "- " + tooltip + Environment.NewLine;
} }
} }
@@ -23,15 +23,17 @@ public sealed partial class IsAntagEligibleCondition : BaseTraitCondition
return ctx.Profile.AntagPreferences.Contains(Antag); return ctx.Profile.AntagPreferences.Contains(Antag);
} }
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc) public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc, int depth)
{ {
if (!proto.TryIndex(Antag, out var antagProto)) if (!proto.TryIndex(Antag, out var antagProto))
return string.Empty; return string.Empty;
var antagName = loc.GetString(antagProto.Name); var antagName = loc.GetString(antagProto.Name);
return Invert var tooltip = Invert
? loc.GetString("trait-condition-antag-not", ("antag", antagName)) ? loc.GetString("trait-condition-antag-not", ("antag", antagName))
: loc.GetString("trait-condition-antag-is", ("antag", antagName)); : loc.GetString("trait-condition-antag-is", ("antag", antagName));
return new string(' ', depth * 2) + "- " + tooltip + Environment.NewLine;
} }
} }
@@ -17,13 +17,13 @@ public sealed partial class IsSpeciesCondition : BaseTraitCondition
protected override bool EvaluateImplementation(TraitConditionContext ctx) protected override bool EvaluateImplementation(TraitConditionContext ctx)
{ {
if (string.IsNullOrEmpty(ctx.SpeciesId)) if (ctx.SpeciesId is not { } speciesId)
return false; return false;
return ctx.SpeciesId == Species; return speciesId == Species;
} }
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc) public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc, int depth)
{ {
var speciesName = Species.Id; var speciesName = Species.Id;
if (proto.TryIndex(Species, out var speciesProto)) if (proto.TryIndex(Species, out var speciesProto))
@@ -31,8 +31,10 @@ public sealed partial class IsSpeciesCondition : BaseTraitCondition
speciesName = loc.GetString(speciesProto.Name); speciesName = loc.GetString(speciesProto.Name);
} }
return Invert var tooltip = Invert
? loc.GetString("trait-condition-species-not", ("species", speciesName)) ? loc.GetString("trait-condition-species-not", ("species", speciesName))
: loc.GetString("trait-condition-species-is", ("species", speciesName)); : loc.GetString("trait-condition-species-is", ("species", speciesName));
return new string(' ', depth * 2) + "- " + tooltip + Environment.NewLine;
} }
} }
@@ -0,0 +1,45 @@
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Body.Systems;
namespace Content.Shared._DV.Traits.Effects;
/// <summary>
/// Effect that removes body parts of the player body.
/// </summary>
public sealed partial class RemoveBodyPartEffect : BaseTraitEffect
{
[DataField(required: true)]
public BodyPartType Part = default;
[DataField(required: true)]
public BodyPartSymmetry Symmetry = default!;
public override void Apply(TraitEffectContext ctx)
{
if (!ctx.EntMan.TryGetComponent(ctx.Player, out BodyComponent? body))
return;
if (!ctx.EntMan.TrySystem<SharedBodySystem>(out var bodySys))
return;
if (!ctx.EntMan.TrySystem<SharedBloodstreamSystem>(out var bloodstreamSys))
return;
if (bodySys.GetRootPartOrNull(ctx.Player, body) is not { } root)
return;
foreach (var part in bodySys.GetBodyChildrenOfType(ctx.Player, Part, body, Symmetry))
{
foreach (var child in bodySys.GetBodyPartChildren(part.Id, part.Component))
{
ctx.EntMan.DeleteEntity(child.Id);
}
ctx.EntMan.DeleteEntity(part.Id);
// apparently chopping off limbs makes people bleed a lot. Who would have guessed?
bloodstreamSys.TryModifyBleedAmount(ctx.Player, -100f);
}
}
}
@@ -51,10 +51,4 @@ public sealed partial class TraitPrototype : IPrototype
/// </summary> /// </summary>
[DataField] [DataField]
public List<BaseTraitEffect> Effects = new(); public List<BaseTraitEffect> Effects = new();
/// <summary>
/// Other traits that are mutually exclusive with this one.
/// </summary>
[DataField]
public List<ProtoId<TraitPrototype>> Conflicts = new();
} }
+11 -13
View File
@@ -10,7 +10,6 @@ disabled-traits-popup-title = Traits Disabled
disabled-traits-popup-label = Traits Disabled disabled-traits-popup-label = Traits Disabled
disabled-traits-popup-message = Some of your selected traits could not be applied because they did not meet the required conditions. disabled-traits-popup-message = Some of your selected traits could not be applied because they did not meet the required conditions.
disabled-traits-popup-list-header = The following traits were disabled: disabled-traits-popup-list-header = The following traits were disabled:
disabled-traits-popup-reason = • {$reason}
disabled-traits-popup-skip-checkbox = Don't show this again disabled-traits-popup-skip-checkbox = Don't show this again
disabled-traits-popup-close-button = Close disabled-traits-popup-close-button = Close
@@ -27,30 +26,29 @@ trait-category-traits-unlimited = {$selected} traits
trait-category-points = ({$selected} / {$max} pts) trait-category-points = ({$selected} / {$max} pts)
## Condition tooltips ## Condition tooltips
trait-conditions-tooltip = [bold]Requirements:[/bold] trait-conditions-tooltip = [bold]Conditions:[/bold]
{$requirements} {$conditions}
trait-conditions-not-met-tooltip = Requirements not met:
{$requirements}
## Composite conditions ## Composite conditions
trait-condition-any-of = Any of the following must be true: trait-condition-any-of = Any of the following must be true:
• {$requirements} {$requirements}
## Species conditions ## Species conditions
trait-condition-species-is = You must be a [color=yellow]{$species}[/color]. trait-condition-species-is = You must be {INDEFINITE($species)} [color=yellow]{$species}[/color].
trait-condition-species-not = You must not be a [color=yellow]{$species}[/color]. trait-condition-species-not = You must not be {INDEFINITE($species)} [color=yellow]{$species}[/color].
## Job conditions ## Job conditions
trait-condition-job-is = You must be a [color={$color}]{$job}[/color]. trait-condition-job-is = You must be {INDEFINITE($job)} [color={$color}]{$job}[/color].
trait-condition-job-not = You must not be a [color={$color}]{$job}[/color]. trait-condition-job-not = You must not be {INDEFINITE($job)} [color={$color}]{$job}[/color].
## Department conditions ## Department conditions
trait-condition-department-is = You must be in the [color={$color}]{$department}[/color] department. 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. trait-condition-department-not = You must not be in the [color={$color}]{$department}[/color] department.
## HasComp borg conditions
trait-condition-borg-not = You must not be a [color=yellow]borg[/color].
# Antag conditions # Antag conditions
trait-condition-antag-is = Must be eligible for [color=red]{$antag}[/color] antag role. 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. trait-condition-antag-not = Must not be eligible for [color=red]{$antag}[/color] antag role.
# Trait requirements and conflicts
trait-condition-trait-has-not = Must not have the trait [color=yellow]{$trait}[/color].
trait-condition-trait-has = Must have the trait [color=yellow]{$trait}[/color].
+13 -2
View File
@@ -15,11 +15,11 @@ trait-irish-accent-desc = Ya sap! Seems you got a pet hate fer rubbish!
trait-ultravision-name = Ultraviolet vision trait-ultravision-name = Ultraviolet vision
trait-ultravision-desc = Whether through custom bionic eyes, random mutation, trait-ultravision-desc = Whether through custom bionic eyes, random mutation,
or being a Harpy, you perceive the world with ultraviolet light. or a bad organ replacement, you perceive the world with ultraviolet light.
trait-deuteranopia-name = Deuteranopia trait-deuteranopia-name = Deuteranopia
trait-deuteranopia-desc = Whether through custom bionic eyes, random mutation, trait-deuteranopia-desc = Whether through custom bionic eyes, random mutation,
or being a Vulpkanin, you have red-green colour blindness. or a bad organ replacement, you have red-green colour blindness.
trait-hushed-name = Hushed trait-hushed-name = Hushed
trait-hushed-desc = You are unable to speak louder than a whisper. trait-hushed-desc = You are unable to speak louder than a whisper.
@@ -52,6 +52,17 @@ trait-amputee-right-arm-desc =
Your right arm is missing! Your right arm is missing!
(Note: Amputee traits do not appear in the character editor preview!) (Note: Amputee traits do not appear in the character editor preview!)
trait-amputee-left-leg-name = Amputee (Leg, Left)
trait-amputee-left-leg-desc =
Your left leg is missing!
(Note: Amputee traits do not appear in the character editor preview!)
trait-amputee-right-leg-name = Amputee (Leg, Right)
trait-amputee-right-leg-desc =
Your right leg is missing!
(Note: Amputee traits do not appear in the character editor preview!)
(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)(Note: Amputee traits do not appear in the character editor preview!)
trait-redshirt-name = Redshirt trait-redshirt-name = Redshirt
trait-redshirt-desc = trait-redshirt-desc =
Sets your death threshold to 100 damage. You no longer have a critical state. Sets your death threshold to 100 damage. You no longer have a critical state.
@@ -6,6 +6,18 @@
description: trait-blindness-desc description: trait-blindness-desc
category: Disabilities category: Disabilities
conditions: conditions:
- !type:HasTraitCondition
trait: PoorVision
invert: true
- !type:HasTraitCondition
trait: Deuteranopia
invert: true
- !type:HasTraitCondition
trait: UltraVision
invert: true
- !type:HasTraitCondition
trait: Monochromacy
invert: true
- !type:HasCompCondition - !type:HasCompCondition
component: Blindable component: Blindable
effects: effects:
@@ -21,6 +33,9 @@
description: trait-poor-vision-desc description: trait-poor-vision-desc
category: Disabilities category: Disabilities
conditions: conditions:
- !type:HasTraitCondition
trait: Blindness
invert: true
- !type:HasCompCondition - !type:HasCompCondition
component: Blindable component: Blindable
effects: effects:
@@ -36,6 +51,16 @@
name: trait-monochromacy-name name: trait-monochromacy-name
description: trait-monochromacy-desc description: trait-monochromacy-desc
category: Disabilities category: Disabilities
conditions:
- !type:HasTraitCondition
trait: Blindness
invert: true
- !type:HasTraitCondition
trait: Deuteranopia
invert: true
- !type:HasTraitCondition
trait: UltraVision
invert: true
effects: effects:
- !type:AddCompsEffect - !type:AddCompsEffect
components: components:
@@ -46,6 +71,19 @@
name: trait-deuteranopia-name name: trait-deuteranopia-name
description: trait-deuteranopia-desc description: trait-deuteranopia-desc
category: Disabilities category: Disabilities
conditions:
- !type:HasTraitCondition
trait: Blindness
invert: true
- !type:HasTraitCondition
trait: Monochromacy
invert: true
- !type:HasTraitCondition
trait: UltraVision
invert: true
- !type:IsSpeciesCondition
species: Vulpkanin
invert: true
effects: effects:
- !type:AddCompsEffect - !type:AddCompsEffect
components: components:
@@ -56,6 +94,19 @@
name: trait-ultravision-name name: trait-ultravision-name
description: trait-ultravision-desc description: trait-ultravision-desc
category: Disabilities category: Disabilities
conditions:
- !type:HasTraitCondition
trait: Blindness
invert: true
- !type:HasTraitCondition
trait: Monochromacy
invert: true
- !type:HasTraitCondition
trait: Deuteranopia
invert: true
- !type:IsSpeciesCondition
species: Harpy
invert: true
effects: effects:
- !type:AddCompsEffect - !type:AddCompsEffect
components: components:
@@ -78,28 +129,40 @@
name: trait-amputee-left-arm-name name: trait-amputee-left-arm-name
description: trait-amputee-left-arm-desc description: trait-amputee-left-arm-desc
category: Disabilities category: Disabilities
conflicts:
- ArmAmputeeRight # TODO: AmputeeComponent doesn't stack, should be an effect instead
effects: effects:
- !type:AddCompsEffect - !type:RemoveBodyPartEffect
components: part: Arm
- type: Amputee symmetry: Left
removeBodyPart: Arm
partSymmetry: Left
- type: trait - type: trait
id: ArmAmputeeRight id: ArmAmputeeRight
name: trait-amputee-right-arm-name name: trait-amputee-right-arm-name
description: trait-amputee-right-arm-desc description: trait-amputee-right-arm-desc
category: Disabilities category: Disabilities
conflicts:
- ArmAmputeeLeft
effects: effects:
- !type:AddCompsEffect - !type:RemoveBodyPartEffect
components: part: Arm
- type: Amputee symmetry: Right
removeBodyPart: Arm
partSymmetry: Right - type: trait
id: LegAmputeeLeft
name: trait-amputee-left-leg-name
description: trait-amputee-left-leg-desc
category: Disabilities
effects:
- !type:RemoveBodyPartEffect
part: Leg
symmetry: Left
- type: trait
id: LegAmputeeRight
name: trait-amputee-right-leg-name
description: trait-amputee-right-leg-desc
category: Disabilities
effects:
- !type:RemoveBodyPartEffect
part: Leg
symmetry: Right
- type: trait - type: trait
id: Dysgraphia id: Dysgraphia
@@ -116,6 +179,10 @@
name: trait-painnumbness-name name: trait-painnumbness-name
description: trait-painnumbness-desc description: trait-painnumbness-desc
category: Disabilities category: Disabilities
conditions:
- !type:IsSpeciesCondition
species: IPC
invert: true
effects: effects:
- !type:AddCompsEffect - !type:AddCompsEffect
components: components:
@@ -126,6 +193,10 @@
name: trait-hemophilia-name name: trait-hemophilia-name
description: trait-hemophilia-desc description: trait-hemophilia-desc
category: Disabilities category: Disabilities
conditions:
- !type:IsSpeciesCondition
species: IPC
invert: true
effects: effects:
- !type:AddCompsEffect - !type:AddCompsEffect
components: components:
@@ -94,10 +94,6 @@
- !type:HasCompCondition - !type:HasCompCondition
component: Silicon component: Silicon
invert: true invert: true
- !type:HasCompCondition
component: BorgChassis
invert: true
tooltip: trait-condition-borg-not
- !type:IsSpeciesCondition - !type:IsSpeciesCondition
species: IPC species: IPC
invert: true invert: true
@@ -52,11 +52,6 @@
description: trait-muted-desc description: trait-muted-desc
category: Accents category: Accents
cost: 2 cost: 2
conditions:
- !type:HasCompCondition
component: BorgChassis
invert: true
tooltip: trait-condition-borg-not
effects: effects:
- !type:AddCompsEffect - !type:AddCompsEffect
components: components:
@@ -165,11 +160,6 @@
description: trait-hushed-desc description: trait-hushed-desc
category: Accents category: Accents
cost: 0 cost: 0
conditions:
- !type:HasCompCondition
component: BorgChassis
invert: true
tooltip: trait-condition-borg-not
effects: effects:
- !type:AddCompsEffect - !type:AddCompsEffect
components: components: