Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Port] End Screen Medals #216

Merged
merged 2 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,63 @@
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))
Vonsant marked this conversation as resolved.
Show resolved Hide resolved
{
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 == null)
Vonsant marked this conversation as resolved.
Show resolved Hide resolved
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];
}
}
136 changes: 136 additions & 0 deletions Content.Server/_CorvaxNext/CrewMedal/CrewMedalSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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;

awardedMedals = awardedMedals.OrderBy(x => x.RecipientName).ToList();
Vonsant marked this conversation as resolved.
Show resolved Hide resolved

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

foreach (var medal in awardedMedals)
{
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
Loading