Delta-v/Content.Client/_DV/Traits/UI/TraitEntry.xaml.cs

392 lines
13 KiB
C#

using Content.Shared._DV.Traits;
using Content.Shared._DV.Traits.Conditions;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._DV.Traits.UI;
[GenerateTypedNameReferences]
public sealed partial class TraitEntry : PanelContainer
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
public event Action<bool>? OnToggled;
public bool IsSelected => TraitCheckbox.Pressed;
public readonly int TraitCost;
public bool MeetsConditions { get; private set; } = true;
private readonly TraitPrototype _trait;
private bool _isUpdating;
private readonly List<string> _failedConditionTooltips = new();
private bool _isLockedByCategory;
private bool _isLockedByPoints;
public TraitEntry(TraitPrototype trait)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_trait = trait;
TraitCost = trait.Cost;
// Enable mouse events so tooltips work
MouseFilter = MouseFilterMode.Pass;
TraitNameLabel.Text = Loc.GetString(trait.Name);
TraitDescriptionLabel.SetMessage(Loc.GetString(trait.Description));
// Format cost display
var costPrefix = trait.Cost > 0 ? "+" : "";
var costColor = trait.Cost > 0 ? "#ff6b6b" : trait.Cost < 0 ? "#6bff6b" : "#888888";
TraitCostLabel.Text = $"{costPrefix}{trait.Cost}";
TraitCostLabel.ModulateSelfOverride = Color.FromHex(costColor);
TraitCheckbox.OnToggled += OnCheckboxToggled;
// Build condition tooltips
UpdateConditionTooltips();
}
private void UpdateConditionTooltips()
{
var tooltips = new List<string>();
foreach (var condition in _trait.Conditions)
{
if (condition is TraitDependencyCondition depCond)
{
// Use GetTooltips() to get individual lines without duplication
tooltips.AddRange(depCond.GetTooltips(_prototype, _loc));
}
else
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
tooltips.Add(tooltip);
}
}
if (tooltips.Count > 0)
{
var tooltipText = Loc.GetString("trait-conditions-tooltip",
("requirements", string.Join("\n", tooltips)));
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
else
TooltipSupplier = null;
}
/// <summary>
/// Creates a tooltip control that properly parses markup.
/// </summary>
private static Tooltip CreateMarkupTooltip(string markupText)
{
var tooltip = new Tooltip();
// Parse the markup into a FormattedMessage
tooltip.SetMessage(FormattedMessage.FromMarkupOrThrow(markupText));
return tooltip;
}
/// <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,
IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
_failedConditionTooltips.Clear();
MeetsConditions = true;
foreach (var condition in _trait.Conditions)
{
bool result;
if (condition is TraitDependencyCondition depCond)
{
result = CheckDependencyCondition(depCond, selectedTraits);
// CheckDependencyCondition adds its own tooltips directly
}
else
{
result = condition switch
{
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences, selectedTraits),
_ => true,
};
// Apply inversion for non-dependency conditions
result ^= condition.Invert;
if (!result)
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
_failedConditionTooltips.Add(tooltip);
}
}
if (!result)
MeetsConditions = false;
}
UpdateDisabledState();
}
/// <summary>
/// Checks a TraitDependencyCondition and adds failure tooltips directly.
/// Returns true if the condition passes.
/// </summary>
private bool CheckDependencyCondition(TraitDependencyCondition condition, IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
var passed = true;
foreach (var conflict in condition.Conflicts)
{
if (selectedTraits == null || !selectedTraits.Contains(conflict))
continue;
if (_prototype.TryIndex(conflict, out var conflictProto))
{
_failedConditionTooltips.Add(_loc.GetString("trait-condition-trait-conflict",
("trait", _loc.GetString(conflictProto.Name))));
}
passed = false;
}
foreach (var required in condition.Requires)
{
if (selectedTraits != null && selectedTraits.Contains(required))
continue;
if (_prototype.TryIndex(required, out var requiredProto))
{
_failedConditionTooltips.Add(_loc.GetString("trait-condition-trait-required",
("trait", _loc.GetString(requiredProto.Name))));
}
passed = false;
}
return passed;
}
private bool CheckSpeciesCondition(IsSpeciesCondition condition, ProtoId<SpeciesPrototype>? speciesId)
{
if (!_prototype.TryIndex(speciesId, out var species))
return false;
return species == condition.Species;
}
private bool CheckJobCondition(HasJobCondition condition, ProtoId<JobPrototype>? jobId)
{
if (!_prototype.TryIndex(jobId, out var job))
return false;
return job == condition.Job;
}
private bool CheckDepartmentCondition(InDepartmentCondition condition, ProtoId<JobPrototype>? jobId)
{
if (!_prototype.TryIndex(jobId, out var job))
return false;
if (!_prototype.TryIndex(condition.Department, out var department))
return false;
return department.Roles.Contains(job);
}
private bool CheckAntagEligibleCondition(IsAntagEligibleCondition condition, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences)
{
if (antagPreferences is null)
return false;
return antagPreferences.Contains(condition.Antag);
}
private bool CheckAnyOfCondition(
AnyOfCondition condition,
ProtoId<JobPrototype>? jobId,
ProtoId<SpeciesPrototype>? speciesId,
IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences,
IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
if (condition.Conditions.Count == 0)
return false;
// Return true if ANY child condition evaluates to true
foreach (var childCondition in condition.Conditions)
{
bool result;
if (childCondition is TraitDependencyCondition depCond)
{
result = CheckDependencyCondition(depCond, selectedTraits);
}
else
{
result = childCondition switch
{
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences, selectedTraits),
_ => true,
};
result ^= childCondition.Invert;
}
// If any child passes, the AnyOf passes
if (result)
return true;
}
// None of the children passed
return false;
}
/// <summary>
/// Called by <see cref="TraitCategory"/> whenever the category's trait or
/// points-cap state changes. Locking only applies to <em>unselected</em>
/// entries - the selected traits are the ones filling the cap and must
/// remain interactive so the player can deselect them.
/// </summary>
public void SetLockedByCategory(bool locked)
{
if (_isLockedByCategory == locked)
return;
_isLockedByCategory = locked;
UpdateDisabledState();
}
/// <summary>
/// Called by <see cref="TraitCategory"/> whenever the player's global point budget
/// changes. Locks unselected entries whose cost the player can no longer afford.
/// Free (0-cost) and negative-cost traits are never locked by this.
/// </summary>
public void SetLockedByPoints(bool locked)
{
if (_isLockedByPoints == locked)
return;
_isLockedByPoints = locked;
UpdateDisabledState();
}
private void UpdateDisabledState()
{
var isSelected = TraitCheckbox.Pressed;
var conditionLocked = !MeetsConditions;
var categoryLocked = _isLockedByCategory && !isSelected;
var pointsLocked = _isLockedByPoints && !isSelected;
var isDisabled = conditionLocked || categoryLocked || pointsLocked;
if (isDisabled)
{
// Hide checkbox, show lock icon
TraitCheckbox.Visible = false;
LockIcon.Visible = true;
if (isSelected && conditionLocked)
{
_isUpdating = true;
TraitCheckbox.Pressed = false;
UpdateSelectedStyle();
_isUpdating = false;
OnToggled?.Invoke(false);
}
// Add disabled styling
AddStyleClass("TraitsEntryDisabled");
// Tooltip priority for conditions: condition failures > category full > insufficient points.
if (conditionLocked && _failedConditionTooltips.Count > 0)
{
var tooltipText = Loc.GetString("trait-conditions-not-met-tooltip",
("requirements", string.Join("\n", _failedConditionTooltips)));
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
else if (categoryLocked)
{
var tooltipText = Loc.GetString("trait-category-full-tooltip");
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
else if (pointsLocked)
{
var tooltipText = Loc.GetString("trait-insufficient-points-tooltip");
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
}
else
{
// Show checkbox, hide lock icon
TraitCheckbox.Visible = true;
LockIcon.Visible = false;
// Remove disabled styling - stylesheet restores normal colors
RemoveStyleClass("TraitsEntryDisabled");
// Reset to normal tooltips
UpdateConditionTooltips();
}
}
private void OnCheckboxToggled(BaseButton.ButtonToggledEventArgs args)
{
if (_isUpdating)
return;
if (!MeetsConditions)
{
// This shouldn't happen since checkbox is hidden, but just in case
_isUpdating = true;
TraitCheckbox.Pressed = false;
_isUpdating = false;
return;
}
UpdateSelectedStyle();
OnToggled?.Invoke(args.Pressed);
}
public void SetSelected(bool selected)
{
_isUpdating = true;
TraitCheckbox.Pressed = selected && MeetsConditions;
UpdateSelectedStyle();
_isUpdating = false;
}
private void UpdateSelectedStyle()
{
if (TraitCheckbox.Pressed)
AddStyleClass("TraitsEntrySelected");
else
RemoveStyleClass("TraitsEntrySelected");
}
}