diff --git a/Content.Client/Ghost/Roles/UI/GhostRoleEntryButtons.xaml b/Content.Client/Ghost/Roles/UI/GhostRoleEntryButtons.xaml
new file mode 100644
index 0000000000..b76cd8821f
--- /dev/null
+++ b/Content.Client/Ghost/Roles/UI/GhostRoleEntryButtons.xaml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/Content.Client/Ghost/Roles/UI/GhostRoleEntryButtons.xaml.cs b/Content.Client/Ghost/Roles/UI/GhostRoleEntryButtons.xaml.cs
new file mode 100644
index 0000000000..4d7c26cfe8
--- /dev/null
+++ b/Content.Client/Ghost/Roles/UI/GhostRoleEntryButtons.xaml.cs
@@ -0,0 +1,9 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.Ghost.Roles.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class GhostRoleEntryButtons : BoxContainer
+{
+}
diff --git a/Content.Client/Ghost/Roles/UI/GhostRolesEntry.xaml b/Content.Client/Ghost/Roles/UI/GhostRolesEntry.xaml
index 483825aae4..d9ed172810 100644
--- a/Content.Client/Ghost/Roles/UI/GhostRolesEntry.xaml
+++ b/Content.Client/Ghost/Roles/UI/GhostRolesEntry.xaml
@@ -7,7 +7,10 @@
-
+
+
+
diff --git a/Content.Client/Ghost/Roles/UI/GhostRolesEntry.xaml.cs b/Content.Client/Ghost/Roles/UI/GhostRolesEntry.xaml.cs
index 159b758e90..bd17a8f0d1 100644
--- a/Content.Client/Ghost/Roles/UI/GhostRolesEntry.xaml.cs
+++ b/Content.Client/Ghost/Roles/UI/GhostRolesEntry.xaml.cs
@@ -9,13 +9,24 @@ namespace Content.Client.Ghost.Roles.UI
[GenerateTypedNameReferences]
public sealed partial class GhostRolesEntry : BoxContainer
{
- public GhostRolesEntry(GhostRoleInfo info, Action requestAction)
+ public event Action? OnRoleSelected;
+ public event Action? OnRoleFollow;
+
+ public GhostRolesEntry(string name, string description, IEnumerable roles)
{
RobustXamlLoader.Load(this);
- Title.Text = info.Name;
- Description.SetMessage(info.Description);
- RequestButton.OnPressed += requestAction;
+ Title.Text = name;
+ Description.SetMessage(description);
+
+ foreach (var role in roles)
+ {
+ var button = new GhostRoleEntryButtons();
+ button.RequestButton.OnPressed += _ => OnRoleSelected?.Invoke(role);
+ button.FollowButton.OnPressed += _ => OnRoleFollow?.Invoke(role);
+
+ Buttons.AddChild(button);
+ }
}
}
}
diff --git a/Content.Client/Ghost/Roles/UI/GhostRolesEui.cs b/Content.Client/Ghost/Roles/UI/GhostRolesEui.cs
index b29d6415ec..49619aa0d0 100644
--- a/Content.Client/Ghost/Roles/UI/GhostRolesEui.cs
+++ b/Content.Client/Ghost/Roles/UI/GhostRolesEui.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using Content.Client.Eui;
using Content.Shared.Eui;
using Content.Shared.Ghost.Roles;
@@ -16,7 +17,7 @@ namespace Content.Client.Ghost.Roles.UI
{
_window = new GhostRolesWindow();
- _window.RoleRequested += info =>
+ _window.OnRoleRequested += info =>
{
if (_windowRules != null)
_windowRules.Close();
@@ -32,6 +33,11 @@ namespace Content.Client.Ghost.Roles.UI
_windowRules.OpenCentered();
};
+ _window.OnRoleFollow += info =>
+ {
+ SendMessage(new GhostRoleFollowRequestMessage(info.Identifier));
+ };
+
_window.OnClose += () =>
{
SendMessage(new GhostRoleWindowCloseMessage());
@@ -56,20 +62,19 @@ namespace Content.Client.Ghost.Roles.UI
base.HandleState(state);
if (state is not GhostRolesEuiState ghostState) return;
-
- var closeRulesWindow = true;
-
_window.ClearEntries();
- foreach (var info in ghostState.GhostRoles)
+ var groupedRoles = ghostState.GhostRoles.GroupBy(
+ role => (role.Name, role.Description));
+ foreach (var group in groupedRoles)
{
- _window.AddEntry(info);
- if (info.Identifier == _windowRulesId)
- {
- closeRulesWindow = false;
- }
+ var name = group.Key.Name;
+ var description = group.Key.Description;
+
+ _window.AddEntry(name, description, group);
}
+ var closeRulesWindow = ghostState.GhostRoles.All(role => role.Identifier != _windowRulesId);
if (closeRulesWindow)
{
_windowRules?.Close();
diff --git a/Content.Client/Ghost/Roles/UI/GhostRolesWindow.xaml.cs b/Content.Client/Ghost/Roles/UI/GhostRolesWindow.xaml.cs
index e52709486d..4b9bc35850 100644
--- a/Content.Client/Ghost/Roles/UI/GhostRolesWindow.xaml.cs
+++ b/Content.Client/Ghost/Roles/UI/GhostRolesWindow.xaml.cs
@@ -8,7 +8,8 @@ namespace Content.Client.Ghost.Roles.UI
[GenerateTypedNameReferences]
public sealed partial class GhostRolesWindow : DefaultWindow
{
- public event Action? RoleRequested;
+ public event Action? OnRoleRequested;
+ public event Action? OnRoleFollow;
public void ClearEntries()
{
@@ -16,10 +17,14 @@ namespace Content.Client.Ghost.Roles.UI
EntryContainer.DisposeAllChildren();
}
- public void AddEntry(GhostRoleInfo info)
+ public void AddEntry(string name, string description, IEnumerable roles)
{
NoRolesMessage.Visible = false;
- EntryContainer.AddChild(new GhostRolesEntry(info, _ => RoleRequested?.Invoke(info)));
+
+ var entry = new GhostRolesEntry(name, description, roles);
+ entry.OnRoleSelected += OnRoleRequested;
+ entry.OnRoleFollow += OnRoleFollow;
+ EntryContainer.AddChild(entry);
}
}
}
diff --git a/Content.Server/Ghost/Roles/GhostRoleSystem.cs b/Content.Server/Ghost/Roles/GhostRoleSystem.cs
index c68b5a1d54..fd0bc98f86 100644
--- a/Content.Server/Ghost/Roles/GhostRoleSystem.cs
+++ b/Content.Server/Ghost/Roles/GhostRoleSystem.cs
@@ -10,6 +10,7 @@ using Content.Server.MobState.States;
using Content.Server.Players;
using Content.Shared.Administration;
using Content.Shared.Database;
+using Content.Shared.Follower;
using Content.Shared.GameTicking;
using Content.Shared.Ghost;
using Content.Shared.Ghost.Roles;
@@ -32,6 +33,7 @@ namespace Content.Server.Ghost.Roles
[Dependency] private readonly EuiManager _euiManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly AdminLogSystem _adminLogSystem = default!;
+ [Dependency] private readonly FollowerSystem _followerSystem = default!;
private uint _nextRoleIdentifier;
private bool _needsUpdateGhostRoleCount = true;
@@ -188,6 +190,14 @@ namespace Content.Server.Ghost.Roles
CloseEui(player);
}
+ public void Follow(IPlayerSession player, uint identifier)
+ {
+ if (!_ghostRoles.TryGetValue(identifier, out var role)) return;
+ if (player.AttachedEntity == null) return;
+
+ _followerSystem.StartFollowingEntity(player.AttachedEntity.Value, role.Owner);
+ }
+
public void GhostRoleInternalCreateMindAndTransfer(IPlayerSession player, EntityUid roleUid, EntityUid mob, GhostRoleComponent? role = null)
{
if (!Resolve(roleUid, ref role)) return;
diff --git a/Content.Server/Ghost/Roles/UI/GhostRolesEui.cs b/Content.Server/Ghost/Roles/UI/GhostRolesEui.cs
index f872f1cbb0..72f425ab12 100644
--- a/Content.Server/Ghost/Roles/UI/GhostRolesEui.cs
+++ b/Content.Server/Ghost/Roles/UI/GhostRolesEui.cs
@@ -21,7 +21,9 @@ namespace Content.Server.Ghost.Roles.UI
case GhostRoleTakeoverRequestMessage req:
EntitySystem.Get().Takeover(Player, req.Identifier);
break;
-
+ case GhostRoleFollowRequestMessage req:
+ EntitySystem.Get().Follow(Player, req.Identifier);
+ break;
case GhostRoleWindowCloseMessage _:
Closed();
break;
diff --git a/Content.Shared/Ghost/Roles/GhostRolesEuiMessages.cs b/Content.Shared/Ghost/Roles/GhostRolesEuiMessages.cs
index 1c6a741029..d8e95d0c53 100644
--- a/Content.Shared/Ghost/Roles/GhostRolesEuiMessages.cs
+++ b/Content.Shared/Ghost/Roles/GhostRolesEuiMessages.cs
@@ -35,6 +35,17 @@ namespace Content.Shared.Ghost.Roles
}
}
+ [NetSerializable, Serializable]
+ public sealed class GhostRoleFollowRequestMessage : EuiMessageBase
+ {
+ public uint Identifier { get; }
+
+ public GhostRoleFollowRequestMessage(uint identifier)
+ {
+ Identifier = identifier;
+ }
+ }
+
[NetSerializable, Serializable]
public sealed class GhostRoleWindowCloseMessage : EuiMessageBase
{
diff --git a/Resources/Locale/en-US/ghost/ghost-gui.ftl b/Resources/Locale/en-US/ghost/ghost-gui.ftl
index 246ff6538a..51bc5529f6 100644
--- a/Resources/Locale/en-US/ghost/ghost-gui.ftl
+++ b/Resources/Locale/en-US/ghost/ghost-gui.ftl
@@ -7,6 +7,7 @@ ghost-target-window-current-button = Warp: {$name}
ghost-roles-window-title = Ghost Roles
ghost-roles-window-request-role-button = Request
+ghost-roles-window-follow-role-button = Follow
ghost-roles-window-no-roles-available-label = There are currently no available ghost roles.
ghost-roles-window-rules-footer = The button will enable after 5 seconds (this delay is to make sure you read the rules).