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)
};
reasonLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(
Loc.GetString("disabled-traits-popup-reason", ("reason", reason))));
reasonLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(reason));
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.
/// 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
+23 -27
View File
@@ -1,3 +1,4 @@
using System.Text;
using Content.Shared._DV.Traits;
using Content.Shared._DV.Traits.Conditions;
using Content.Shared.Humanoid.Prototypes;
@@ -25,7 +26,6 @@ public sealed partial class TraitEntry : PanelContainer
private readonly TraitPrototype _trait;
private bool _isUpdating;
private readonly List<string> _failedConditionTooltips = new();
public TraitEntry(TraitPrototype trait)
{
@@ -55,24 +55,23 @@ public sealed partial class TraitEntry : PanelContainer
private void UpdateConditionTooltips()
{
var tooltips = new List<string>();
var conditionsTooltip = new StringBuilder();
foreach (var condition in _trait.Conditions)
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
tooltips.Add(tooltip);
conditionsTooltip.Append(condition.GetTooltip(_prototype, _loc, 0));
}
if (tooltips.Count > 0)
if (conditionsTooltip.Length > 0)
{
var tooltipText = Loc.GetString("trait-conditions-tooltip",
("requirements", string.Join("\n", tooltips)));
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
var tooltip = Loc.GetString("trait-conditions-tooltip", ("conditions", conditionsTooltip.ToString())).ToString().Trim();
var markupTooltip = CreateMarkupTooltip(tooltip);
TooltipSupplier = _ => markupTooltip;
}
else
{
TooltipSupplier = null;
}
}
/// <summary>
@@ -91,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)
{
_failedConditionTooltips.Clear();
MeetsConditions = true;
foreach (var condition in _trait.Conditions)
@@ -105,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,
};
@@ -115,9 +114,6 @@ public sealed partial class TraitEntry : PanelContainer
if (!result)
{
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);
}
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;
@@ -173,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,
};
@@ -210,15 +215,6 @@ public sealed partial class TraitEntry : PanelContainer
// Add disabled styling
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
{
+3 -13
View File
@@ -1,7 +1,6 @@
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;
@@ -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);
_currentTraitCount++;
_currentPointsSpent += trait.Cost;
@@ -177,6 +166,7 @@ public sealed partial class TraitsTab : BoxContainer
UpdateGlobalStats();
UpdateCategoryStats(trait.Category);
UpdateAllConditions();
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 (parentWidth > 0)
{
GlobalPointsBar.SetWidth = (int)((parentWidth - 2 ) * percentage);
GlobalPointsBar.SetWidth = (int)((parentWidth - 2) * percentage);
_awaitingLayoutUpdate = false;
}
else if (!_awaitingLayoutUpdate)
@@ -281,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();
@@ -6,7 +6,10 @@ using Content.Shared._DV.Traits.Conditions;
using Content.Shared._DV.Traits.Effects;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Nutrition.Components;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -20,7 +23,7 @@ namespace Content.IntegrationTests.Tests._DV;
/// Tests all conditions, effects, and validation logic.
/// </summary>
[TestFixture]
[TestOf(typeof(TraitSystemTest))]
[TestOf(typeof(TraitSystem))]
public sealed partial class TraitSystemTest
{
[TestPrototypes]
@@ -107,8 +110,10 @@ public sealed partial class TraitSystemTest
description: trait-dysgraphia-name
category: TestCategoryUnlimited
cost: 0
conflicts:
- TestTraitConflictB
conditions:
- !type:HasTraitCondition
trait: TestTraitConflictB
invert: true
effects:
- !type:AddCompsEffect
components:
@@ -582,31 +587,29 @@ public sealed partial class TraitSystemTest
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ProtoMan;
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
{
"TestTraitConflictA",
"TestTraitConflictB",
};
var profile = new HumanoidCharacterProfile()
.WithTraitPreference("TestTraitConflictA", protoMan)
.WithTraitPreference("TestTraitConflictB", protoMan);
var traitSys = entMan.System<TraitSystem>();
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
BindingFlags.NonPublic | BindingFlags.Instance);
var disabledTraits = new Dictionary<ProtoId<TraitPrototype>, List<string>>();
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.That(validTraits?.Count, Is.EqualTo(1), "Only one conflicting trait should be valid");
Assert.That(validTraits.Contains("TestTraitConflictA"), Is.True, "First trait should be kept");
Assert.That(validTraits.Contains("TestTraitConflictB"),
Is.False,
"Conflicting trait should be rejected");
Assert.That(validTraits.Contains("TestTraitConflictB"), Is.True, "Non-complaining trait should be kept");
Assert.That(disabledTraits.ContainsKey("TestTraitConflictA"), Is.True, "Trait with conflicts should be rejected");
});
entMan.DeleteEntity(player);
@@ -639,7 +642,7 @@ public sealed partial class TraitSystemTest
BindingFlags.NonPublic | BindingFlags.Instance);
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");
@@ -669,6 +672,7 @@ public sealed partial class TraitSystemTest
};
var traitSys = entMan.System<TraitSystem>();
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
BindingFlags.NonPublic | BindingFlags.Instance);
@@ -760,8 +764,8 @@ public sealed partial class TraitSystemTest
IPrototypeManager protoMan,
IComponentFactory factory,
EntityUid player,
string? jobId = null,
string? speciesId = null)
ProtoId<JobPrototype>? jobId = null,
ProtoId<SpeciesPrototype>? speciesId = null)
{
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,
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))
{
@@ -246,7 +211,7 @@ public sealed class TraitSystem : EntitySystem
continue;
// 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))
rejectionReasons.Add(tooltip);
@@ -1,3 +1,4 @@
using System.Text;
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.Traits.Conditions;
@@ -38,27 +39,25 @@ public sealed partial class AnyOfCondition : BaseTraitCondition
return false;
}
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc)
public override string GetTooltip(IPrototypeManager proto, ILocalizationManager loc, int depth)
{
if (Conditions.Count == 0)
return string.Empty;
var requirements = new List<string>();
var requirementsTooltip = new StringBuilder();
foreach (var condition in Conditions)
{
var tooltip = condition.GetTooltip(proto, loc);
if (!string.IsNullOrEmpty(tooltip))
requirements.Add(tooltip);
var conditionTooltip = condition.GetTooltip(proto, loc, depth + 1);
if (conditionTooltip.Length > 0)
requirementsTooltip.Append(conditionTooltip);
}
if (requirements.Count == 0)
if (requirementsTooltip.Length == 0)
return string.Empty;
// AAAAAAAAAAA
var joinedRequirements = string.Join("\n• ", requirements);
var tooltip = loc.GetString("trait-condition-any-of", ("requirements", requirementsTooltip));
// Handle inversion in tooltip (ANY becomes NONE when inverted)
return Loc.GetString("trait-condition-any-of", ("requirements", joinedRequirements));
return new string(' ', depth * 2) + "- " + tooltip;
}
}
@@ -1,4 +1,6 @@
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using JetBrains.Annotations;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@@ -31,7 +33,7 @@ public abstract partial class BaseTraitCondition
/// Generates a human-readable tooltip describing this condition's requirements.
/// </summary>
[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);
}
@@ -52,12 +54,12 @@ public sealed class TraitConditionContext
/// <summary>
/// The job ID of the player, if available.
/// </summary>
public string? JobId { get; init; }
public ProtoId<JobPrototype>? JobId { get; init; }
/// <summary>
/// The species ID of the player, if available.
/// </summary>
public string? SpeciesId { get; init; }
public ProtoId<SpeciesPrototype>? SpeciesId { get; init; }
/// <summary>
/// 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 (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
return string.Empty;
@@ -17,13 +17,13 @@ public sealed partial class HasJobCondition : BaseTraitCondition
protected override bool EvaluateImplementation(TraitConditionContext ctx)
{
if (string.IsNullOrEmpty(ctx.JobId))
if (ctx.JobId is not { } jobId)
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 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-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)
{
if (string.IsNullOrEmpty(ctx.JobId))
if (ctx.JobId is not { } jobId)
return false;
if (!ctx.Proto.TryIndex(Department, out var department))
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 deptColor = "#ffffff";
@@ -37,9 +37,10 @@ public sealed partial class InDepartmentCondition : BaseTraitCondition
deptColor = deptProto.Color.ToHex();
}
return Invert
var tooltip = Invert
? loc.GetString("trait-condition-department-not", ("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);
}
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))
return string.Empty;
var antagName = loc.GetString(antagProto.Name);
return Invert
var tooltip = Invert
? 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)
{
if (string.IsNullOrEmpty(ctx.SpeciesId))
if (ctx.SpeciesId is not { } speciesId)
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;
if (proto.TryIndex(Species, out var speciesProto))
@@ -31,8 +31,10 @@ public sealed partial class IsSpeciesCondition : BaseTraitCondition
speciesName = loc.GetString(speciesProto.Name);
}
return Invert
var tooltip = Invert
? loc.GetString("trait-condition-species-not", ("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>
[DataField]
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-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-reason = • {$reason}
disabled-traits-popup-skip-checkbox = Don't show this again
disabled-traits-popup-close-button = Close
@@ -27,30 +26,29 @@ trait-category-traits-unlimited = {$selected} traits
trait-category-points = ({$selected} / {$max} pts)
## Condition tooltips
trait-conditions-tooltip = [bold]Requirements:[/bold]
{$requirements}
trait-conditions-not-met-tooltip = Requirements not met:
{$requirements}
trait-conditions-tooltip = [bold]Conditions:[/bold]
{$conditions}
## Composite conditions
trait-condition-any-of = Any of the following must be true:
• {$requirements}
{$requirements}
## Species conditions
trait-condition-species-is = You must be a [color=yellow]{$species}[/color].
trait-condition-species-not = You must not be a [color=yellow]{$species}[/color].
trait-condition-species-is = You must be {INDEFINITE($species)} [color=yellow]{$species}[/color].
trait-condition-species-not = You must not be {INDEFINITE($species)} [color=yellow]{$species}[/color].
## Job conditions
trait-condition-job-is = You must be a [color={$color}]{$job}[/color].
trait-condition-job-not = You must not be a [color={$color}]{$job}[/color].
trait-condition-job-is = You must be {INDEFINITE($job)} [color={$color}]{$job}[/color].
trait-condition-job-not = You must not be {INDEFINITE($job)} [color={$color}]{$job}[/color].
## Department conditions
trait-condition-department-is = You must be in the [color={$color}]{$department}[/color] department.
trait-condition-department-not = You must not be in the [color={$color}]{$department}[/color] department.
## 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: