392 lines
13 KiB
C#
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");
|
|
}
|
|
}
|