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
This commit is contained in:
Tobias Berger
2026-01-30 03:12:03 +01:00
parent 51d605f67b
commit 333948591f
14 changed files with 209 additions and 239 deletions
@@ -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.
/// </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)
{
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences);
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences, traits);
}
// Update stats since some traits may have been deselected
+19 -61
View File
@@ -26,7 +26,6 @@ public sealed partial class TraitEntry : PanelContainer
private readonly TraitPrototype _trait;
private bool _isUpdating;
private string _failedConditionTooltip = string.Empty;
public TraitEntry(TraitPrototype trait)
{
@@ -56,54 +55,17 @@ public sealed partial class TraitEntry : PanelContainer
private void UpdateConditionTooltips()
{
var tooltip = new StringBuilder();
var sectionTooltip = new StringBuilder();
var conditionsTooltip = new StringBuilder();
#region Requirements
foreach (var requirementId in _trait.Requirements)
{
if (!_prototype.TryIndex(requirementId, out var requirement))
continue;
sectionTooltip.Append("- ");
sectionTooltip.AppendLine(Loc.GetString(requirement.Name));
}
if (sectionTooltip.Length > 0)
tooltip.AppendLine(Loc.GetString("trait-requirements-tooltip", ("requirements", sectionTooltip.ToString())));
sectionTooltip.Clear();
#endregion Requirements
#region Conflicts
foreach (var requirementId in _trait.Conflicts)
{
if (!_prototype.TryIndex(requirementId, out var requirement))
continue;
sectionTooltip.Append("- ");
sectionTooltip.AppendLine(Loc.GetString(requirement.Name));
}
if (sectionTooltip.Length > 0)
tooltip.AppendLine(Loc.GetString("trait-conflicts-tooltip", ("conflicts", sectionTooltip.ToString())));
sectionTooltip.Clear();
#endregion Conflicts
#region Conditions
foreach (var condition in _trait.Conditions)
{
sectionTooltip.Append(condition.GetTooltip(_prototype, _loc, 0));
conditionsTooltip.Append(condition.GetTooltip(_prototype, _loc, 0));
}
if (sectionTooltip.Length > 0)
tooltip.Append(Loc.GetString("trait-conditions-tooltip", ("conditions", sectionTooltip.ToString())));
sectionTooltip.Clear();
#endregion Conditions
if (tooltip.Length > 0)
if (conditionsTooltip.Length > 0)
{
var markupTooltip = CreateMarkupTooltip(tooltip.ToString().Trim());
var tooltip = Loc.GetString("trait-conditions-tooltip", ("conditions", conditionsTooltip.ToString())).ToString().Trim();
var markupTooltip = CreateMarkupTooltip(tooltip);
TooltipSupplier = _ => markupTooltip;
}
else
@@ -128,9 +90,8 @@ public sealed partial class TraitEntry : PanelContainer
/// <summary>
/// Updates whether conditions are met based on current job/species.
/// </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)
{
var tooltipBuilder = new StringBuilder();
MeetsConditions = true;
foreach (var condition in _trait.Conditions)
@@ -142,7 +103,8 @@ public sealed partial class TraitEntry : PanelContainer
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby but screws with the inversion logic
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,
};
@@ -152,12 +114,8 @@ public sealed partial class TraitEntry : PanelContainer
if (!result)
{
MeetsConditions = false;
var conditionTooltip = condition.GetTooltip(_prototype, _loc, 0);
if (!string.IsNullOrEmpty(conditionTooltip))
tooltipBuilder.Append(conditionTooltip);
}
}
_failedConditionTooltip = tooltipBuilder.ToString().Trim();
UpdateDisabledState();
}
@@ -197,7 +155,15 @@ public sealed partial class TraitEntry : PanelContainer
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)
return false;
@@ -211,8 +177,9 @@ 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, antagPreferences), // Recursive!
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
HasTraitCondition hasTrait => CheckHasTraitCondition(hasTrait, traits),
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences, traits), // Recursive!
_ => true,
};
@@ -248,15 +215,6 @@ public sealed partial class TraitEntry : PanelContainer
// Add disabled styling
AddStyleClass("TraitsEntryDisabled");
// Update tooltip to show failed conditions
if (_failedConditionTooltip.Length > 0)
{
var tooltipText = Loc.GetString("trait-conditions-not-met-tooltip",
("conditions", _failedConditionTooltip));
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
}
else
{
+3 -12
View File
@@ -153,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);
_currentTraitCount++;
_currentPointsSpent += trait.Cost;
@@ -176,6 +166,7 @@ public sealed partial class TraitsTab : BoxContainer
UpdateGlobalStats();
UpdateCategoryStats(trait.Category);
UpdateAllConditions();
OnTraitsChanged?.Invoke(_selectedTraits);
}
@@ -210,7 +201,7 @@ public sealed partial class TraitsTab : BoxContainer
// If parent width is 0 (not laid out yet), defer until layout happens
if (parentWidth > 0)
{
GlobalPointsBar.SetWidth = (int)((parentWidth - 2 ) * percentage);
GlobalPointsBar.SetWidth = (int)((parentWidth - 2) * percentage);
_awaitingLayoutUpdate = false;
}
else if (!_awaitingLayoutUpdate)
@@ -280,7 +271,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, _profile?.Species, _profile?.AntagPreferences);
categoryUi.UpdateConditions(null, _profile?.Species, _profile?.AntagPreferences, _selectedTraits);
}
RecalculateStats();
@@ -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);
}
}
}
+2 -37
View File
@@ -81,8 +81,8 @@ public sealed class TraitSystem : EntitySystem
EntityUid player,
IReadOnlySet<ProtoId<TraitPrototype>> selectedTraits,
ICommonSession? session,
string? jobId,
string? speciesId,
ProtoId<JobPrototype>? jobId,
ProtoId<SpeciesPrototype>? speciesId,
HumanoidCharacterProfile? profile,
Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
{
@@ -142,41 +142,6 @@ public sealed class TraitSystem : EntitySystem
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
if (!CheckConditions(trait, conditionCtx, rejectionReasons))
{
@@ -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;
}
}
@@ -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,16 +51,4 @@ public sealed partial class TraitPrototype : IPrototype
/// </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();
/// <summary>
/// Other traits that are required to pick this one.
/// </summary>
[DataField]
public List<ProtoId<TraitPrototype>> Requirements = new();
}
@@ -28,8 +28,6 @@ trait-category-points = ({$selected} / {$max} pts)
## Condition tooltips
trait-conditions-tooltip = [bold]Conditions:[/bold]
{$conditions}
trait-conditions-not-met-tooltip = [bold]Conditions not met:[/bold]
{$conditions}
## Composite conditions
trait-condition-any-of = Any of the following must be true:
@@ -47,9 +45,10 @@ trait-condition-job-not = You must not be {INDEFINITE($job)} [color={$color}]{$j
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.
## 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.
# 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-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-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-desc = You are unable to speak louder than a whisper.
@@ -52,6 +52,17 @@ trait-amputee-right-arm-desc =
Your right arm is missing!
(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-desc =
Sets your death threshold to 100 damage. You no longer have a critical state.
@@ -6,6 +6,18 @@
description: trait-blindness-desc
category: Disabilities
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
component: Blindable
effects:
@@ -21,6 +33,9 @@
description: trait-poor-vision-desc
category: Disabilities
conditions:
- !type:HasTraitCondition
trait: Blindness
invert: true
- !type:HasCompCondition
component: Blindable
effects:
@@ -36,6 +51,16 @@
name: trait-monochromacy-name
description: trait-monochromacy-desc
category: Disabilities
conditions:
- !type:HasTraitCondition
trait: Blindness
invert: true
- !type:HasTraitCondition
trait: Deuteranopia
invert: true
- !type:HasTraitCondition
trait: UltraVision
invert: true
effects:
- !type:AddCompsEffect
components:
@@ -46,6 +71,19 @@
name: trait-deuteranopia-name
description: trait-deuteranopia-desc
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:
- !type:AddCompsEffect
components:
@@ -56,6 +94,19 @@
name: trait-ultravision-name
description: trait-ultravision-desc
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:
- !type:AddCompsEffect
components:
@@ -78,28 +129,40 @@
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:RemoveBodyPartEffect
part: Arm
symmetry: 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:RemoveBodyPartEffect
part: Arm
symmetry: 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
id: Dysgraphia
@@ -116,6 +179,10 @@
name: trait-painnumbness-name
description: trait-painnumbness-desc
category: Disabilities
conditions:
- !type:IsSpeciesCondition
species: IPC
invert: true
effects:
- !type:AddCompsEffect
components:
@@ -126,6 +193,10 @@
name: trait-hemophilia-name
description: trait-hemophilia-desc
category: Disabilities
conditions:
- !type:IsSpeciesCondition
species: IPC
invert: true
effects:
- !type:AddCompsEffect
components:
@@ -94,10 +94,6 @@
- !type:HasCompCondition
component: Silicon
invert: true
- !type:HasCompCondition
component: BorgChassis
invert: true
tooltip: trait-condition-borg-not
- !type:IsSpeciesCondition
species: IPC
invert: true
@@ -52,11 +52,6 @@
description: trait-muted-desc
category: Accents
cost: 2
conditions:
- !type:HasCompCondition
component: BorgChassis
invert: true
tooltip: trait-condition-borg-not
effects:
- !type:AddCompsEffect
components:
@@ -165,11 +160,6 @@
description: trait-hushed-desc
category: Accents
cost: 0
conditions:
- !type:HasCompCondition
component: BorgChassis
invert: true
tooltip: trait-condition-borg-not
effects:
- !type:AddCompsEffect
components: