[Raid] Add FC and role prompt when joining a raid

This commit is contained in:
2022-11-26 12:22:08 +01:00
parent e334e02768
commit 21803eb728
13 changed files with 373 additions and 27 deletions

View File

@@ -0,0 +1,9 @@
using System.Diagnostics.CodeAnalysis;
namespace Cocotte.Modules.Raids;
public interface IPlayerInfosRepository
{
bool TryGetPlayerInfo(ulong playerId, [MaybeNullWhen(false)] out PlayerInfo playerInfo);
void UpdatePlayerInfo(PlayerInfo playerInfo);
}

View File

@@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
namespace Cocotte.Modules.Raids;
public class MemoryPlayerInfosRepository : IPlayerInfosRepository
{
private readonly IDictionary<ulong, PlayerInfo> _playerInfos;
public MemoryPlayerInfosRepository()
{
_playerInfos = new Dictionary<ulong, PlayerInfo>();
}
public bool TryGetPlayerInfo(ulong playerId, [MaybeNullWhen(false)] out PlayerInfo playerInfo)
{
return _playerInfos.TryGetValue(playerId, out playerInfo);
}
public void UpdatePlayerInfo(PlayerInfo playerInfo)
{
_playerInfos[playerInfo.Id] = playerInfo;
}
}

View File

@@ -0,0 +1,31 @@
namespace Cocotte.Modules.Raids;
public class PlayerInfo
{
public static TimeSpan FcUpdateInterval { get; } = TimeSpan.FromDays(3);
public ulong Id { get; }
public uint Fc
{
get => _fc;
set
{
_fc = value;
_lastFcUpdate = DateTime.Today;
}
}
public bool IsFcUpdateRequired => DateTime.Today - _lastFcUpdate > FcUpdateInterval;
private uint _fc;
private DateTime _lastFcUpdate;
public PlayerInfo(ulong id, uint fc)
{
Id = id;
Fc = fc;
_lastFcUpdate = DateTime.Today;
}
}

View File

@@ -21,9 +21,14 @@ public class Raid
#endif
}
public bool AddPlayer(ulong id, string name, PlayerRole role, int fc, bool substitute = false)
public bool AddPlayer(RosterPlayer player)
{
return _rosterManager.AddPlayer(new RosterPlayer(id, name, role, fc, substitute));
return _rosterManager.AddPlayer(player);
}
public bool UpdatePlayer(RosterPlayer rosterPlayer)
{
return _rosterManager.UpdatePlayer(rosterPlayer);
}
public RosterPlayer GetPlayer(ulong id)
@@ -31,6 +36,11 @@ public class Raid
return _rosterManager.GetPlayer(id);
}
public bool ContainsPlayer(ulong userId)
{
return _rosterManager.ContainsPlayer(userId);
}
public bool RemovePlayer(ulong id)
{
return _rosterManager.RemovePlayer(id);

View File

@@ -13,7 +13,7 @@ public class RaidFormatter
_rolesOptions = rolesOptions;
}
private string RoleToEmote(PlayerRole role) => role switch
public string RoleToEmote(PlayerRole role) => role switch
{
PlayerRole.Dps => _rolesOptions.DpsEmote,
PlayerRole.Tank => _rolesOptions.TankEmote,
@@ -21,7 +21,7 @@ public class RaidFormatter
_ => ":question:"
};
public static string FcFormat(int fc) => fc switch
public static string FcFormat(uint fc) => fc switch
{
< 1_000 => $"{fc}",
_ => $"{fc/1000}k"

View File

@@ -13,14 +13,19 @@ namespace Cocotte.Modules.Raids;
public class RaidModule : InteractionModuleBase<SocketInteractionContext>
{
private readonly ILogger<RaidModule> _logger;
private readonly IRaidsRepository _raidsRepository;
private readonly IRaidsRepository _raids;
private readonly IPlayerInfosRepository _playerInfos;
private readonly RaidFormatter _raidFormatter;
private readonly RaidRegisterManager _registerManager;
public RaidModule(ILogger<RaidModule> logger, IRaidsRepository raidsRepository, RaidFormatter raidFormatter)
public RaidModule(ILogger<RaidModule> logger, IRaidsRepository raids, IPlayerInfosRepository playerInfos,
RaidFormatter raidFormatter, RaidRegisterManager registerManager)
{
_logger = logger;
_raidsRepository = raidsRepository;
_raids = raids;
_playerInfos = playerInfos;
_raidFormatter = raidFormatter;
_registerManager = registerManager;
}
[EnabledInDm(false)]
@@ -47,12 +52,12 @@ public class RaidModule : InteractionModuleBase<SocketInteractionContext>
_logger.LogInformation("Created new raid with id {RaidId}", raidId);
// Calculate date
var date = DateTime.Now.Date;
var date = DateTime.Today;
date = date.AddDays(DateTimeUtils.CalculateDayOfWeekOffset(date.DayOfWeek, day))
.Add(timeOnly.ToTimeSpan());
// New raid instance
if (!_raidsRepository.AddNewRaid(raidId, date))
if (!_raids.AddNewRaid(raidId, date))
{
// A raid with this message id already exists, how??
_logger.LogWarning("Tried to create a new raid with already existing id: {RaidId}", raidId);
@@ -67,7 +72,7 @@ public class RaidModule : InteractionModuleBase<SocketInteractionContext>
}
// Build the raid message
var raid = _raidsRepository[raidId];
var raid = _raids[raidId];
var components = RaidComponents(raidId);
var embed = _raidFormatter.RaidEmbed(raid);
@@ -80,9 +85,9 @@ public class RaidModule : InteractionModuleBase<SocketInteractionContext>
}
[ComponentInteraction("raid raid_join:*", true)]
public async Task Join(ulong raidId)
public async Task RaidJoin(ulong raidId)
{
if (!_raidsRepository.TryGetRaid(raidId, out var raid))
if (!_raids.TryGetRaid(raidId, out var raid))
{
await RespondAsync(
ephemeral: true,
@@ -92,9 +97,10 @@ public class RaidModule : InteractionModuleBase<SocketInteractionContext>
return;
}
// Todo: Ask role, FC, substitute
var user = (IGuildUser) Context.User;
if (!raid.AddPlayer(user.Id, user.DisplayName, PlayerRole.Dps, 10000))
// Check if player is already registered early
if (raid.ContainsPlayer(user.Id))
{
await RespondAsync(
ephemeral: true,
@@ -103,19 +109,154 @@ public class RaidModule : InteractionModuleBase<SocketInteractionContext>
return;
}
_logger.LogInformation("User {User} joined raid {Raid}", Context.User.Username, raid);
// Ask player info
_registerManager.RegisteringPlayers[(raidId, user.Id)] = new RosterPlayer(user.Id, user.DisplayName);
await UpdateRaidRosterEmbed(raid);
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.SuccessEmbed($"Successfully joined the raid as {raid.GetPlayer(user.Id).Role}").Build()
);
_logger.LogDebug("User {User} is registering for raid {Raid}", user.Username, raid);
// Add name
await RespondAsync($"Please select a role for raid", components: PlayerRoleComponent(raid, user).Build(), ephemeral: true);
}
[ComponentInteraction("raid player_select_role:*:*", ignoreGroupNames: true)]
public async Task PlayerSelectRole(ulong raidId, ulong playerId, string selectedRoleInput)
{
var selectedRole = Enum.Parse<PlayerRole>(selectedRoleInput);
if (_registerManager.RegisteringPlayers.TryGetValue((raidId, playerId), out var rosterPlayer))
{
_registerManager.RegisteringPlayers[(raidId, playerId)] = rosterPlayer with {Role = selectedRole};
await RespondAsync();
}
// The user is not currently registering, wonder how he got here then
else
{
await RespondAsync(ephemeral: true,
embed: EmbedUtils.ErrorEmbed("You are not registering for this raid :thinking:").Build());
}
}
[ComponentInteraction("raid player_join:*:*", ignoreGroupNames: true)]
public async Task PlayerJoinNonSubstitute(ulong raidId, ulong playerId)
{
await PlayerJoin(raidId, playerId, false);
}
[ComponentInteraction("raid player_join_substitute:*:*", ignoreGroupNames: true)]
public async Task PlayerJoinSubstitute(ulong raidId, ulong playerId)
{
await PlayerJoin(raidId, playerId, true);
}
private async Task PlayerJoin(ulong raidId, ulong playerId, bool substitute)
{
// Check if player is registering
if (!_registerManager.RegisteringPlayers.TryGetValue((raidId, playerId), out var rosterPlayer))
{
await RespondAsync(ephemeral: true,
embed: EmbedUtils.ErrorEmbed("You are not registering for this raid :thinking:").Build());
return;
}
// Check if we need to ask FC
if (!_playerInfos.TryGetPlayerInfo(playerId, out var playerInfo))
{
await Context.Interaction.RespondWithModalAsync<FcModal>($"raid modal_fc:{raidId}");
return;
}
// Player already has FC registered but it's outdated
if (playerInfo.IsFcUpdateRequired)
{
await Context.Interaction.RespondWithModalAsync<FcModal>($"raid modal_fc:{raidId}",
modifyModal: m =>
m.UpdateTextInput("fc", component => component.Value = playerInfo.Fc.FormatSpaced())
);
return;
}
// Register user for raid
await RegisterPlayer(raidId, rosterPlayer with { Fc = rosterPlayer.Fc, Substitute = substitute});
}
[ModalInteraction("raid modal_fc:*", ignoreGroupNames: true)]
public async Task ModalFcSubmit(ulong raidId, FcModal modal)
{
var playerId = Context.User.Id;
_logger.LogTrace("Received modal FC modal from {User} with value: {Fc}", Context.User.Username, modal.Fc);
// Check if player is registering
if (!_registerManager.RegisteringPlayers.TryGetValue((raidId, playerId), out var rosterPlayer))
{
await RespondAsync(ephemeral: true,
embed: EmbedUtils.ErrorEmbed("You are not registering for this raid :thinking:").Build());
return;
}
var fcInput = modal.Fc.Replace(" ", "");
if (!uint.TryParse(fcInput, out var fc))
{
await RespondAsync(ephemeral: true,
embed: EmbedUtils.ErrorEmbed("Invalid fc, try registering again").Build());
_registerManager.RegisteringPlayers.Remove((raidId, playerId));
return;
}
_playerInfos.UpdatePlayerInfo(new PlayerInfo(playerId, fc));
await RegisterPlayer(raidId, rosterPlayer with { Fc = fc });
}
private async Task RegisterPlayer(ulong raidId, RosterPlayer rosterPlayer)
{
// Check if raid exists
if (!_raids.TryGetRaid(raidId, out var raid))
{
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.InfoEmbed("This raid does not exist").Build());
return;
}
_registerManager.RegisteringPlayers[(raidId, Context.User.Id)] = rosterPlayer;
// Player is already registered, update info
if (!raid.AddPlayer(rosterPlayer))
{
raid.UpdatePlayer(rosterPlayer);
await UpdateRaidRosterEmbed(raid);
// await RespondAsync(
// ephemeral: true,
// embed: EmbedUtils.SuccessEmbed($"Successfully update you're role to: {rosterPlayer.Role}").Build());
await RespondAsync();
}
// It's a new player
else
{
_logger.LogInformation("User {User} joined raid {Raid}", Context.User.Username, raid);
await UpdateRaidRosterEmbed(raid);
await RespondAsync(
ephemeral: true,
embed: EmbedUtils.SuccessEmbed($"Successfully joined the raid as {raid.GetPlayer(rosterPlayer.Id).Role}").Build()
);
}
}
[ComponentInteraction("raid raid_leave:*", ignoreGroupNames: true)]
public async Task Leave(ulong raidId)
{
if (!_raidsRepository.TryGetRaid(raidId, out var raid))
if (!_raids.TryGetRaid(raidId, out var raid))
{
await RespondAsync(
ephemeral: true,
@@ -155,7 +296,7 @@ public class RaidModule : InteractionModuleBase<SocketInteractionContext>
}
}
private ComponentBuilder RaidComponents(ulong raidId)
private static ComponentBuilder RaidComponents(ulong raidId)
{
return new ComponentBuilder()
.AddRow(new ActionRowBuilder()
@@ -171,4 +312,38 @@ public class RaidModule : InteractionModuleBase<SocketInteractionContext>
)
);
}
private ComponentBuilder PlayerRoleComponent(Raid raid, IGuildUser user)
{
var select = new SelectMenuBuilder()
.WithPlaceholder(PlayerRole.Dps.ToString())
.WithCustomId($"raid player_select_role:{raid.Id}:{user.Id}")
.WithMinValues(1)
.WithMaxValues(1);
foreach (var role in Enum.GetValues<PlayerRole>())
{
// TODO add emote
select.AddOption(role.ToString(), role.ToString());
}
return new ComponentBuilder()
.AddRow(new ActionRowBuilder()
.WithSelectMenu(select)
)
.AddRow(new ActionRowBuilder()
.WithButton("Join", $"raid player_join:{raid.Id}:{user.Id}")
.WithButton("Join substitute", $"raid player_join_substitute:{raid.Id}:{user.Id}")
);
}
}
public class FcModal : IModal
{
public string Title => "Please enter your FC";
[NotNull]
[InputLabel("FC")]
[ModalTextInput("fc", placeholder: "30 000", minLength: 1, maxLength: 7)]
public string? Fc { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace Cocotte.Modules.Raids;
public class RaidRegisterManager
{
public IDictionary<(ulong raidId, ulong playerId), RosterPlayer> RegisteringPlayers =
new Dictionary<(ulong raidId, ulong playerId), RosterPlayer>();
}

View File

@@ -14,6 +14,18 @@ public class RosterManager
return _players.TryAdd(rosterPlayer.Id, rosterPlayer);
}
public bool UpdatePlayer(RosterPlayer rosterPlayer)
{
if (!_players.ContainsKey(rosterPlayer.Id))
{
return false;
}
_players[rosterPlayer.Id] = rosterPlayer;
return true;
}
public bool RemovePlayer(ulong id)
{
return _players.Remove(id);
@@ -23,4 +35,9 @@ public class RosterManager
{
return _players[id];
}
public bool ContainsPlayer(ulong userId)
{
return _players.ContainsKey(userId);
}
}

View File

@@ -1,6 +1,6 @@
namespace Cocotte.Modules.Raids;
public record RosterPlayer(ulong Id, string Name, PlayerRole Role, int Fc, bool Substitute = false)
public record RosterPlayer(ulong Id, string Name, PlayerRole Role = PlayerRole.Dps, uint Fc = 0, bool Substitute = false)
{
public int RosterNumber { get; set; }

View File

@@ -34,10 +34,12 @@ IHost host = Host.CreateDefaultBuilder(args)
// Data
services.AddSingleton<IRaidsRepository, MemoryRaidRepository>();
services.AddSingleton<IPlayerInfosRepository, MemoryPlayerInfosRepository>();
services.AddSingleton<RolesOptions>();
// Raids
services.AddSingleton<RaidFormatter>();
services.AddSingleton<RaidRegisterManager>();
// Custom
services.AddSingleton<SharedCounter>();

View File

@@ -0,0 +1,34 @@
using Discord;
namespace Cocotte.Utils;
public static class ModalExtensions
{
public static ModalBuilder UpdateTextInput(this ModalBuilder modal, string customId, Action<TextInputBuilder> inputUpdater)
{
var components = modal.Components.ActionRows.SelectMany(r => r.Components).OfType<TextInputComponent>();
var component = components.First(c => c.CustomId == customId);
var builder = new TextInputBuilder
{
CustomId = customId,
Label = component.Label,
MaxLength = component.MaxLength,
MinLength = component.MinLength,
Placeholder = component.Placeholder,
Required = component.Required,
Style = component.Style,
Value = component.Value
};
inputUpdater(builder);
foreach (var row in modal.Components.ActionRows.Where(row => row.Components.Any(c => c.CustomId == customId)))
{
row.Components.RemoveAll(c => c.CustomId == customId);
row.AddComponent(builder.Build());
}
return modal;
}
}

View File

@@ -0,0 +1,38 @@
using System.Numerics;
using System.Text;
namespace Cocotte.Utils;
public static class NumberUtils
{
public static string FormatSpaced<T>(this INumber<T> number) where T : INumber<T>?
{
var stringNumber = number.ToString(null, null);
Span<char> result = stackalloc char[stringNumber.Length + stringNumber.Length / 3];
int resultOffset = 0;
for (int i = 0; i < stringNumber.Length; i++)
{
// Add a space
if (i > 2 && (i + 1) % 3 == 1)
{
result[resultOffset] = ' ';
resultOffset++;
}
result[resultOffset] = stringNumber[^(i + 1)];
resultOffset++;
}
var realResult = result[..resultOffset];
Span<char> reversed = stackalloc char[resultOffset];
for (int i = 0; i < reversed.Length; i++)
{
reversed[i] = realResult[^(i + 1)];
}
return reversed.ToString();
}
}

View File

@@ -6,9 +6,9 @@ public static class RaidExtensions
{
public static void AddTestPlayers(this Raid raid)
{
raid.AddPlayer(0, "YamaRaja", PlayerRole.Healer, 30000, false);
raid.AddPlayer(1, "Zaku", PlayerRole.Dps, 40000, false);
raid.AddPlayer(2, "Juchi", PlayerRole.Tank, 40000, false);
raid.AddPlayer(3, "Akeno", PlayerRole.Dps, 40000, true);
raid.AddPlayer(new RosterPlayer(0, "YamaRaja", PlayerRole.Healer, 30000, false));
raid.AddPlayer(new RosterPlayer(1, "Zaku", PlayerRole.Dps, 40000, false));
raid.AddPlayer(new RosterPlayer(2, "Juchi", PlayerRole.Tank, 40000, false));
raid.AddPlayer(new RosterPlayer(3, "Akeno", PlayerRole.Dps, 40000, true));
}
}