Skip to content

Commit

Permalink
[Port] End Screen Medals (#216)
Browse files Browse the repository at this point in the history
* Medals

* Fixes
  • Loading branch information
Vonsant authored Jan 2, 2025
1 parent f502cdf commit 277a19b
Show file tree
Hide file tree
Showing 10 changed files with 525 additions and 0 deletions.
34 changes: 34 additions & 0 deletions Content.Client/_CorvaxNext/CrewMedal/CrewMedalSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Content.Client._CorvaxNext.CrewMedal.UI;
using Content.Shared._CorvaxNext.CrewMedal;

namespace Content.Client._CorvaxNext.CrewMedal;

/// <summary>
/// Handles the client-side logic for the Crew Medal system.
/// </summary>
public sealed class CrewMedalSystem : SharedCrewMedalSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _userInterfaceSystem = default!;

public override void Initialize()
{
base.Initialize();
// Subscribes to the event triggered after the state is automatically handled.
SubscribeLocalEvent<CrewMedalComponent, AfterAutoHandleStateEvent>(OnCrewMedalAfterState);
}

/// <summary>
/// When an updated state is received on the client, refresh the UI to display the latest data.
/// </summary>
private void OnCrewMedalAfterState(Entity<CrewMedalComponent> entity, ref AfterAutoHandleStateEvent args)
{
// Checks if the Crew Medal UI is open for the given entity and reloads it with updated data.
if (_userInterfaceSystem.TryGetOpenUi<CrewMedalBoundUserInterface>(
entity.Owner,
CrewMedalUiKey.Key,
out var medalUi))
{
medalUi.Reload();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Content.Shared._CorvaxNext.CrewMedal;
using Robust.Client.UserInterface;

namespace Content.Client._CorvaxNext.CrewMedal.UI;

/// <summary>
/// A wrapper class for the Crew Medal user interface.
/// Initializes the <see cref="CrewMedalWindow"/> and updates it when new data is received from the server.
/// </summary>
public sealed class CrewMedalBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IEntityManager _entityManager = default!;

/// <summary>
/// The main interface window.
/// </summary>
[ViewVariables]
private CrewMedalWindow? _window;

public CrewMedalBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}

protected override void Open()
{
base.Open();

_window = this.CreateWindow<CrewMedalWindow>();
_window.OnReasonChanged += HandleReasonChanged;

Reload();
}

/// <summary>
/// Called when the reason is changed in the <see cref="CrewMedalWindow"/>.
/// Sends a message to the server with the new reason if it differs from the current one.
/// </summary>
private void HandleReasonChanged(string newReason)
{
if (!_entityManager.TryGetComponent<CrewMedalComponent>(Owner, out var component))
return;

if (!component.Reason.Equals(newReason))
{
SendPredictedMessage(new CrewMedalReasonChangedMessage(newReason));
}
}

/// <summary>
/// Updates the data in the window to reflect the current state of the <see cref="CrewMedalComponent"/>.
/// </summary>
public void Reload()
{
if (_window is null)
return;

if (!_entityManager.TryGetComponent<CrewMedalComponent>(Owner, out var component))
return;

_window.SetCurrentReason(component.Reason);
_window.SetAwarded(component.Awarded);
_window.SetMaxCharacters(component.MaxCharacters);
}
}
13 changes: 13 additions & 0 deletions Content.Client/_CorvaxNext/CrewMedal/UI/CrewMedalWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Loc 'crew-medal-ui-header'}">
<BoxContainer Orientation="Vertical" SeparationOverride="4" MinWidth="150">
<Label Text="{Loc 'crew-medal-ui-reason'}" />
<BoxContainer Orientation="Horizontal">
<LineEdit Name="ReasonLineEdit" HorizontalExpand="True" />
<Button Name="SaveButton" Text="{Loc 'crew-medal-ui-save'}" />
</BoxContainer>
<Label Name="CharacterLabel" Text="" />
<Label Text="{Loc 'crew-medal-ui-info'}" />
</BoxContainer>
</DefaultWindow>
88 changes: 88 additions & 0 deletions Content.Client/_CorvaxNext/CrewMedal/UI/CrewMedalWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;

namespace Content.Client._CorvaxNext.CrewMedal.UI;

[GenerateTypedNameReferences]
public sealed partial class CrewMedalWindow : DefaultWindow
{
/// <summary>
/// Event triggered when the "Save" button is pressed,
/// provided the user has changed the reason text.
/// </summary>
public event Action<string>? OnReasonChanged;

private bool _isFocused;
private string _reason = string.Empty;
private bool _awarded;
private int _maxCharacters = 50;

public CrewMedalWindow()
{
RobustXamlLoader.Load(this);

ReasonLineEdit.OnTextChanged += _ =>
{
// Check character limit and award status
SaveButton.Disabled = _awarded || ReasonLineEdit.Text.Length > _maxCharacters;
CharacterLabel.Text = Loc.GetString(
"crew-medal-ui-character-limit",
("number", ReasonLineEdit.Text.Length),
("max", _maxCharacters));
};

ReasonLineEdit.OnFocusEnter += _ => _isFocused = true;
ReasonLineEdit.OnFocusExit += _ => _isFocused = false;

SaveButton.OnPressed += _ =>
{
OnReasonChanged?.Invoke(ReasonLineEdit.Text);
SaveButton.Disabled = true;
};

// Initialize the character counter display
CharacterLabel.Text = Loc.GetString(
"crew-medal-ui-character-limit",
("number", ReasonLineEdit.Text.Length),
("max", _maxCharacters));
}

/// <summary>
/// Sets the current reason and synchronizes it with the input field
/// if the user is not currently editing the field.
/// </summary>
public void SetCurrentReason(string reason)
{
if (_reason == reason)
return;

_reason = reason;

// Synchronize text if the input field is not focused
if (!_isFocused)
ReasonLineEdit.Text = _reason;
}

/// <summary>
/// Updates the "is medal awarded" status
/// and disables editing if the medal is already awarded.
/// </summary>
public void SetAwarded(bool awarded)
{
_awarded = awarded;
ReasonLineEdit.Editable = !_awarded;
SaveButton.Disabled = _awarded;
}

/// <summary>
/// Updates the maximum character limit for the reason.
/// If the current text exceeds the limit, it will be truncated.
/// </summary>
public void SetMaxCharacters(int number)
{
_maxCharacters = number;
if (ReasonLineEdit.Text.Length > _maxCharacters)
ReasonLineEdit.Text = ReasonLineEdit.Text[.._maxCharacters];
}
}
137 changes: 137 additions & 0 deletions Content.Server/_CorvaxNext/CrewMedal/CrewMedalSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using Content.Server.GameTicking;
using Content.Shared.Administration.Logs;
using Content.Shared.Clothing;
using Content.Shared._CorvaxNext.CrewMedal;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.Popups;
using System.Linq;
using System.Text;

namespace Content.Server._CorvaxNext.CrewMedal;

public sealed class CrewMedalSystem : SharedCrewMedalSystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;

public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CrewMedalComponent, ClothingGotEquippedEvent>(OnMedalEquipped);
SubscribeLocalEvent<CrewMedalComponent, CrewMedalReasonChangedMessage>(OnMedalReasonChanged);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
}

/// <summary>
/// Called when a medal is equipped on a character, indicating the medal has been awarded.
/// </summary>
private void OnMedalEquipped(Entity<CrewMedalComponent> medal, ref ClothingGotEquippedEvent args)
{
if (medal.Comp.Awarded)
return;

medal.Comp.Recipient = Identity.Name(args.Wearer, EntityManager);
medal.Comp.Awarded = true;
Dirty(medal);

// Display a popup about the award
_popupSystem.PopupEntity(
Loc.GetString(
"comp-crew-medal-award-text",
("recipient", medal.Comp.Recipient),
("medal", Name(medal.Owner))
),
medal.Owner
);

// Log the event
_adminLogger.Add(
LogType.Action,
LogImpact.Low,
$"{ToPrettyString(args.Wearer):player} was awarded the {ToPrettyString(medal.Owner):entity} with the reason \"{medal.Comp.Reason}\"."
);
}

/// <summary>
/// Called when the reason is updated in the interface (before the medal is awarded).
/// </summary>
private void OnMedalReasonChanged(EntityUid uid, CrewMedalComponent medalComp, CrewMedalReasonChangedMessage args)
{
if (medalComp.Awarded)
return;

// Trim to the character limit and sanitize the input
var maxLength = Math.Min(medalComp.MaxCharacters, args.Reason.Length);
medalComp.Reason = Sanitize(args.Reason[..maxLength]);

Dirty(uid, medalComp);

// Log the update
_adminLogger.Add(
LogType.Action,
LogImpact.Low,
$"{ToPrettyString(args.Actor):user} set {ToPrettyString(uid):entity} with award reason \"{medalComp.Reason}\"."
);
}

/// <summary>
/// Adds a list of awarded medals to the round-end summary window.
/// </summary>
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
var awardedMedals = new List<(string MedalName, string RecipientName, string Reason)>();

var query = EntityQueryEnumerator<CrewMedalComponent>();
while (query.MoveNext(out var uid, out var component))
{
if (component.Awarded)
{
awardedMedals.Add(
(Name(uid), component.Recipient, component.Reason)
);
}
}

if (awardedMedals.Count == 0)
return;

// Sort and convert to array
var sortedMedals = awardedMedals.OrderBy(x => x.RecipientName).ToArray();

var result = new StringBuilder();
result.AppendLine(
Loc.GetString(
"comp-crew-medal-round-end-result",
("count", sortedMedals.Length)
)
);

foreach (var medal in sortedMedals)
{
result.AppendLine(
Loc.GetString(
"comp-crew-medal-round-end-list",
("medal", Sanitize(medal.MedalName)),
("recipient", Sanitize(medal.RecipientName)),
("reason", Sanitize(medal.Reason))
)
);
}

ev.AddLine(result.AppendLine().ToString());
}

/// <summary>
/// Removes certain prohibited characters (e.g., brackets)
/// to prevent unwanted tags in the text.
/// </summary>
private string Sanitize(string input)
{
return input
.Replace("[", string.Empty)
.Replace("]", string.Empty)
.Replace("{", string.Empty)
.Replace("}", string.Empty);
}
}
39 changes: 39 additions & 0 deletions Content.Shared/_CorvaxNext/CrewMedal/CrewMedalComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Robust.Shared.GameStates;

namespace Content.Shared._CorvaxNext.CrewMedal;

/// <summary>
/// Component for a medal that can be awarded to a player and
/// will be displayed in the final round summary screen.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class CrewMedalComponent : Component
{
/// <summary>
/// The name of the recipient of the award.
/// </summary>
[AutoNetworkedField]
[DataField]
public string Recipient = string.Empty;

/// <summary>
/// The reason for the award. Can be set before the medal is awarded.
/// </summary>
[AutoNetworkedField]
[DataField]
public string Reason = string.Empty;

/// <summary>
/// If <c>true</c>, the medal is considered awarded, and the reason can no longer be changed.
/// </summary>
[AutoNetworkedField]
[DataField]
public bool Awarded;

/// <summary>
/// The maximum number of characters allowed for the reason.
/// </summary>
[AutoNetworkedField]
[DataField]
public int MaxCharacters = 50;
}
Loading

0 comments on commit 277a19b

Please sign in to comment.