Compare commits
2 Commits
12507b0fed
...
194feb94dd
| Author | SHA1 | Date | |
|---|---|---|---|
| 194feb94dd | |||
| 0535655f58 |
@@ -14,10 +14,6 @@ public class Raid
|
|||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
DateTime = dateTime;
|
DateTime = dateTime;
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
this.AddTestPlayers();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AddPlayer(RosterPlayer rosterPlayer)
|
public bool AddPlayer(RosterPlayer rosterPlayer)
|
||||||
@@ -55,6 +51,11 @@ public class Raid
|
|||||||
return _players.Remove(id);
|
return _players.Remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AssignRosters(RosterAssigner assigner)
|
||||||
|
{
|
||||||
|
assigner.AssignRosters(_players.Values, 8);
|
||||||
|
}
|
||||||
|
|
||||||
public override bool Equals(object? other)
|
public override bool Equals(object? other)
|
||||||
{
|
{
|
||||||
return other is Raid roster && roster.Id == Id;
|
return other is Raid roster && roster.Id == Id;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Cocotte.Options;
|
using System.Numerics;
|
||||||
|
using Cocotte.Options;
|
||||||
using Cocotte.Utils;
|
using Cocotte.Utils;
|
||||||
using Discord;
|
using Discord;
|
||||||
|
|
||||||
@@ -21,10 +22,10 @@ public class RaidFormatter
|
|||||||
_ => ":question:".ToEmote()
|
_ => ":question:".ToEmote()
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string FcFormat(uint fc) => fc switch
|
public static string FcFormat<T>(T fc) where T : IBinaryInteger<T> => fc switch
|
||||||
{
|
{
|
||||||
< 1_000 => $"{fc}",
|
< 1_000 => $"{fc}",
|
||||||
_ => $"{fc/1000}k"
|
_ => $"{fc/T.CreateChecked(1000)}k"
|
||||||
};
|
};
|
||||||
|
|
||||||
public string FormatRosterPlayer(RosterPlayer player) => player.Substitute switch
|
public string FormatRosterPlayer(RosterPlayer player) => player.Substitute switch
|
||||||
@@ -41,12 +42,11 @@ public class RaidFormatter
|
|||||||
var nonSubstitute = rosterPlayers.Where(p => !p.Substitute);
|
var nonSubstitute = rosterPlayers.Where(p => !p.Substitute);
|
||||||
var substitute = rosterPlayers.Where(p => p.Substitute);
|
var substitute = rosterPlayers.Where(p => p.Substitute);
|
||||||
|
|
||||||
var separatorLength = Math.Max(nonSubstitute.Select(p => p.Name.Length).Max(), substitute.Select(p => p.Name.Length).Max());
|
var separatorLength = players.Select(p => p.Name.Length).Max();
|
||||||
separatorLength = (int) ((separatorLength + 13) * 0.49); // Don't ask why, it just works
|
separatorLength = (int) ((separatorLength + 5) * 1.31); // Don't ask why, it just works
|
||||||
|
|
||||||
// Todo add Total FC number
|
|
||||||
return new EmbedFieldBuilder()
|
return new EmbedFieldBuilder()
|
||||||
.WithName($"Roster {rosterNumber}")
|
.WithName($"Roster {rosterNumber} ({FcFormat(nonSubstitute.Sum(p => p.Fc))} FC)")
|
||||||
.WithValue($"{string.Join("\n", nonSubstitute.Select(FormatRosterPlayer))}\n{new string('━', separatorLength)}\n{string.Join("\n", substitute.Select(FormatRosterPlayer))}")
|
.WithValue($"{string.Join("\n", nonSubstitute.Select(FormatRosterPlayer))}\n{new string('━', separatorLength)}\n{string.Join("\n", substitute.Select(FormatRosterPlayer))}")
|
||||||
.WithIsInline(true);
|
.WithIsInline(true);
|
||||||
}
|
}
|
||||||
@@ -55,6 +55,6 @@ public class RaidFormatter
|
|||||||
.WithColor(Colors.CocotteBlue)
|
.WithColor(Colors.CocotteBlue)
|
||||||
.WithTitle(":crossed_swords: Raid")
|
.WithTitle(":crossed_swords: Raid")
|
||||||
.WithDescription($"**Date:** {TimestampTag.FromDateTime(raid.DateTime, TimestampTagStyles.LongDateTime)}")
|
.WithDescription($"**Date:** {TimestampTag.FromDateTime(raid.DateTime, TimestampTagStyles.LongDateTime)}")
|
||||||
.WithFields(raid.Rosters.Select(r => RosterEmbed(r.Key, r)));
|
.WithFields(raid.Rosters.OrderBy(r => r.Key).Select(r => RosterEmbed(r.Key, r)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,15 +17,17 @@ public partial class RaidModule : InteractionModuleBase<SocketInteractionContext
|
|||||||
private readonly IPlayerInfosRepository _playerInfos;
|
private readonly IPlayerInfosRepository _playerInfos;
|
||||||
private readonly RaidFormatter _raidFormatter;
|
private readonly RaidFormatter _raidFormatter;
|
||||||
private readonly RaidRegisterManager _registerManager;
|
private readonly RaidRegisterManager _registerManager;
|
||||||
|
private readonly RosterAssigner _rosterAssigner;
|
||||||
|
|
||||||
public RaidModule(ILogger<RaidModule> logger, IRaidsRepository raids, IPlayerInfosRepository playerInfos,
|
public RaidModule(ILogger<RaidModule> logger, IRaidsRepository raids, IPlayerInfosRepository playerInfos,
|
||||||
RaidFormatter raidFormatter, RaidRegisterManager registerManager)
|
RaidFormatter raidFormatter, RaidRegisterManager registerManager, RosterAssigner rosterAssigner)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_raids = raids;
|
_raids = raids;
|
||||||
_playerInfos = playerInfos;
|
_playerInfos = playerInfos;
|
||||||
_raidFormatter = raidFormatter;
|
_raidFormatter = raidFormatter;
|
||||||
_registerManager = registerManager;
|
_registerManager = registerManager;
|
||||||
|
_rosterAssigner = rosterAssigner;
|
||||||
}
|
}
|
||||||
|
|
||||||
[EnabledInDm(false)]
|
[EnabledInDm(false)]
|
||||||
@@ -293,6 +295,8 @@ public partial class RaidModule : InteractionModuleBase<SocketInteractionContext
|
|||||||
|
|
||||||
if (message is SocketUserMessage userMessage)
|
if (message is SocketUserMessage userMessage)
|
||||||
{
|
{
|
||||||
|
raid.AssignRosters(_rosterAssigner);
|
||||||
|
|
||||||
await userMessage.ModifyAsync(
|
await userMessage.ModifyAsync(
|
||||||
m => m.Embed = _raidFormatter.RaidEmbed(raid).Build()
|
m => m.Embed = _raidFormatter.RaidEmbed(raid).Build()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,68 @@ public partial class RaidModule
|
|||||||
await AddTestPlayer(message, PlayerRole.Healer);
|
await AddTestPlayer(message, PlayerRole.Healer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MessageCommand("Fill roster")]
|
||||||
|
public async Task FillRoster(IMessage message)
|
||||||
|
{
|
||||||
|
if (message is IUserMessage userMessage && userMessage.Author.IsBot)
|
||||||
|
{
|
||||||
|
if (_raids.TryGetRaid(userMessage.Id, out var raid))
|
||||||
|
{
|
||||||
|
// Add 3 healers
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
raid.AddPlayer(new RosterPlayer(
|
||||||
|
(ulong) Random.Shared.NextInt64(),
|
||||||
|
$"Healer{Random.Shared.Next(1, 100)}",
|
||||||
|
PlayerRole.Healer,
|
||||||
|
(uint) (1000 * Random.Shared.Next(30, 60)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 3 tanks
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
raid.AddPlayer(new RosterPlayer(
|
||||||
|
(ulong) Random.Shared.NextInt64(),
|
||||||
|
$"Tank{Random.Shared.Next(1, 100)}",
|
||||||
|
PlayerRole.Tank,
|
||||||
|
(uint) (1000 * Random.Shared.Next(30, 60)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 8 dps
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
raid.AddPlayer(new RosterPlayer(
|
||||||
|
(ulong) Random.Shared.NextInt64(),
|
||||||
|
$"Dps{Random.Shared.Next(1, 100)}",
|
||||||
|
PlayerRole.Dps,
|
||||||
|
(uint) (1000 * Random.Shared.Next(30, 60)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill rest with substitutes
|
||||||
|
for (int i = 0; i < 6; i++)
|
||||||
|
{
|
||||||
|
raid.AddPlayer(new RosterPlayer(
|
||||||
|
(ulong) Random.Shared.NextInt64(),
|
||||||
|
$"Dps{Random.Shared.Next(1, 100)}",
|
||||||
|
PlayerRole.Dps,
|
||||||
|
(uint) (1000 * Random.Shared.Next(30, 60)),
|
||||||
|
true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UpdateRaidRosterEmbed(raid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await RespondAsync(
|
||||||
|
embed: EmbedUtils.SuccessEmbed($"Successfully filled the roster").Build(),
|
||||||
|
ephemeral: true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task AddTestPlayer(IMessage message, PlayerRole playerRole)
|
private async Task AddTestPlayer(IMessage message, PlayerRole playerRole)
|
||||||
{
|
{
|
||||||
if (message is IUserMessage userMessage && userMessage.Author.IsBot)
|
if (message is IUserMessage userMessage && userMessage.Author.IsBot)
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
|
using Cocotte.Utils;
|
||||||
|
|
||||||
namespace Cocotte.Modules.Raids;
|
namespace Cocotte.Modules.Raids;
|
||||||
|
|
||||||
public class RosterAssigner
|
public class RosterAssigner
|
||||||
{
|
{
|
||||||
public void AssignRosters(IEnumerable<RosterPlayer> players, uint playersPerRoster)
|
public void AssignRosters(IEnumerable<RosterPlayer> players, uint playersPerRoster)
|
||||||
{
|
{
|
||||||
// Start by grouping players
|
// Start by grouping players
|
||||||
var groups = GroupPlayers(players.OrderByDescending(p => p.Fc));
|
var groups = GroupPlayers(players.OrderByDescending(p => p.Fc));
|
||||||
|
|
||||||
// Create rosters
|
// Create rosters
|
||||||
var neededRosters = (int)Math.Ceiling(players.Count(p => !p.Substitute) / (double)playersPerRoster);
|
var neededRosters = (int)Math.Ceiling(players.Count(p => !p.Substitute) / (double)playersPerRoster);
|
||||||
var rosters = new List<RosterInfo>(Enumerable.Repeat(new RosterInfo(), neededRosters));
|
var rosters = Enumerable.Range(0, neededRosters).Select(_ => new RosterInfo()).ToList();
|
||||||
|
|
||||||
// Todo Check when there's more than max players per roster
|
// Todo Check when there's more than max players per roster
|
||||||
|
|
||||||
// First pass: assign healers and tanks
|
// First pass: assign healers and tanks
|
||||||
// Always assign to the group which have the least amount of healer/tank, biased towards healers
|
// Always assign to the group which have the least amount of healer/tank, biased towards healers
|
||||||
// Skip groups without players
|
// Skip groups without players
|
||||||
@@ -21,15 +23,15 @@ public class RosterAssigner
|
|||||||
{
|
{
|
||||||
if (group.Players.AnyHealer())
|
if (group.Players.AnyHealer())
|
||||||
{
|
{
|
||||||
var nextHealerRoster = rosters.MinBy(r => r.RealHealerCount());
|
var nextHealerRoster = rosters.MinBy(r => r.RealHealerCount(), (x, y) => x.TotalRealFc > y.TotalRealFc ? y : x);
|
||||||
|
|
||||||
nextHealerRoster!.AddGroup(group);
|
nextHealerRoster.AddGroup(group);
|
||||||
}
|
}
|
||||||
else if (group.Players.AnyTank())
|
else if (group.Players.AnyTank())
|
||||||
{
|
{
|
||||||
var nextTankRoster = rosters.MinBy(r => r.RealTankCount());
|
var nextTankRoster = rosters.MinBy(r => r.RealTankCount(), (x, y) => x.TotalRealFc < y.TotalRealFc ? x : y);
|
||||||
|
|
||||||
nextTankRoster!.AddGroup(group);
|
nextTankRoster.AddGroup(group);
|
||||||
}
|
}
|
||||||
// Those groups will be used to assign dps, they should still be in descending order of FC
|
// Those groups will be used to assign dps, they should still be in descending order of FC
|
||||||
else
|
else
|
||||||
@@ -37,30 +39,59 @@ public class RosterAssigner
|
|||||||
dpsGroup.Add(group);
|
dpsGroup.Add(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Third pass: assign dps
|
// Third pass: assign dps
|
||||||
foreach (var group in dpsGroup)
|
foreach (var group in dpsGroup)
|
||||||
{
|
{
|
||||||
var nextDpsRoster = rosters.MinBy(r => r.TotalRealFc);
|
var nextDpsRoster = rosters.MinBy(r => r.TotalRealFc);
|
||||||
|
|
||||||
|
nextDpsRoster!.AddGroup(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last pass: do the same but with substitutes
|
||||||
|
dpsGroup = new List<PlayerGroup>();
|
||||||
|
foreach (var group in groups.Where(g => g.AllSubstitutes))
|
||||||
|
{
|
||||||
|
if (group.Substitutes.AnyHealer())
|
||||||
|
{
|
||||||
|
var nextHealerRoster = rosters.MinBy(r => r.TotalHealerCount(), (x, y) => x.TotalFc > y.TotalFc ? y : x);
|
||||||
|
|
||||||
|
nextHealerRoster.AddGroup(group);
|
||||||
|
}
|
||||||
|
else if (group.Substitutes.AnyTank())
|
||||||
|
{
|
||||||
|
var nextTankRoster = rosters.MinBy(r => r.TotalTankCount(), (x, y) => x.TotalFc < y.TotalFc ? x : y);
|
||||||
|
|
||||||
|
nextTankRoster.AddGroup(group);
|
||||||
|
}
|
||||||
|
// Those groups will be used to assign dps, they should still be in descending order of FC
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dpsGroup.Add(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third pass: assign dps
|
||||||
|
foreach (var group in dpsGroup)
|
||||||
|
{
|
||||||
|
var nextDpsRoster = rosters.MinBy(r => r.TotalFc);
|
||||||
|
|
||||||
nextDpsRoster!.AddGroup(group);
|
nextDpsRoster!.AddGroup(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last pass: fill with substitute
|
|
||||||
|
|
||||||
// Assign rosters
|
// Assign rosters
|
||||||
for (int i = 0; i < rosters.Count; i++)
|
for (int i = 0; i < rosters.Count; i++)
|
||||||
{
|
{
|
||||||
var roster = rosters[i];
|
var roster = rosters[i];
|
||||||
|
|
||||||
roster.AssignRosterNumer(i);
|
roster.AssignRosterNumer(i + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IList<PlayerGroup> GroupPlayers(IEnumerable<RosterPlayer> players)
|
private IList<PlayerGroup> GroupPlayers(IEnumerable<RosterPlayer> players)
|
||||||
{
|
{
|
||||||
var groups = new List<PlayerGroup>();
|
var groups = new List<PlayerGroup>();
|
||||||
|
|
||||||
// Todo create groups from player preferences
|
// Todo create groups from player preferences
|
||||||
foreach (var rosterPlayer in players)
|
foreach (var rosterPlayer in players)
|
||||||
{
|
{
|
||||||
@@ -90,7 +121,7 @@ public class RosterInfo
|
|||||||
{
|
{
|
||||||
_groups.Add(group);
|
_groups.Add(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AssignRosterNumer(int rosterNumber)
|
public void AssignRosterNumer(int rosterNumber)
|
||||||
{
|
{
|
||||||
foreach (var group in _groups)
|
foreach (var group in _groups)
|
||||||
@@ -115,7 +146,7 @@ public class PlayerGroup
|
|||||||
{
|
{
|
||||||
_players = new List<RosterPlayer>();
|
_players = new List<RosterPlayer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerGroup(params RosterPlayer[] players)
|
public PlayerGroup(params RosterPlayer[] players)
|
||||||
{
|
{
|
||||||
_players = players;
|
_players = players;
|
||||||
@@ -125,7 +156,7 @@ public class PlayerGroup
|
|||||||
{
|
{
|
||||||
_players.Add(player);
|
_players.Add(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AssignRosterNumer(int rosterNumber)
|
public void AssignRosterNumer(int rosterNumber)
|
||||||
{
|
{
|
||||||
foreach (var rosterPlayer in _players)
|
foreach (var rosterPlayer in _players)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ public static class RosterExtensions
|
|||||||
{
|
{
|
||||||
return players.Any(p => p.Role == PlayerRole.Healer);
|
return players.Any(p => p.Role == PlayerRole.Healer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool AnyTank(this IEnumerable<RosterPlayer> players)
|
public static bool AnyTank(this IEnumerable<RosterPlayer> players)
|
||||||
{
|
{
|
||||||
return players.Any(p => p.Role == PlayerRole.Tank);
|
return players.Any(p => p.Role == PlayerRole.Tank);
|
||||||
@@ -16,24 +16,34 @@ public static class RosterExtensions
|
|||||||
{
|
{
|
||||||
return players.Count(p => p.Role == PlayerRole.Healer);
|
return players.Count(p => p.Role == PlayerRole.Healer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int TankCount(this IEnumerable<RosterPlayer> players)
|
public static int TankCount(this IEnumerable<RosterPlayer> players)
|
||||||
{
|
{
|
||||||
return players.Count(p => p.Role == PlayerRole.Tank);
|
return players.Count(p => p.Role == PlayerRole.Tank);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long TotalFc(this IEnumerable<RosterPlayer> players)
|
public static long TotalFc(this IEnumerable<RosterPlayer> players)
|
||||||
{
|
{
|
||||||
return players.Sum(p => p.Fc);
|
return players.Sum(p => p.Fc);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int RealHealerCount(this RosterInfo rosterInfo)
|
public static int RealHealerCount(this RosterInfo rosterInfo)
|
||||||
{
|
{
|
||||||
return rosterInfo.PlayerGroups.Sum(g => g.Players.HealerCount());
|
return rosterInfo.PlayerGroups.Sum(g => g.Players.HealerCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int TotalHealerCount(this RosterInfo rosterInfo)
|
||||||
|
{
|
||||||
|
return rosterInfo.PlayerGroups.Concat(rosterInfo.SubstituteGroups).Sum(g => g.Players.Concat(g.Substitutes).HealerCount());
|
||||||
|
}
|
||||||
|
|
||||||
public static int RealTankCount(this RosterInfo rosterInfo)
|
public static int RealTankCount(this RosterInfo rosterInfo)
|
||||||
{
|
{
|
||||||
return rosterInfo.PlayerGroups.Sum(g => g.Players.TankCount());
|
return rosterInfo.PlayerGroups.Sum(g => g.Players.TankCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int TotalTankCount(this RosterInfo rosterInfo)
|
||||||
|
{
|
||||||
|
return rosterInfo.PlayerGroups.Concat(rosterInfo.SubstituteGroups).Sum(g => g.Players.Concat(g.Substitutes).TankCount());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
31
Cocotte/Utils/EnumerableExtensions.cs
Normal file
31
Cocotte/Utils/EnumerableExtensions.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
namespace Cocotte.Utils;
|
||||||
|
|
||||||
|
public static class EnumerableExtensions
|
||||||
|
{
|
||||||
|
public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
|
||||||
|
Func<TSource, TSource, TSource> conflictResolver)
|
||||||
|
{
|
||||||
|
var comparer = Comparer<TKey>.Default;
|
||||||
|
|
||||||
|
var min = source.First();
|
||||||
|
var minKey = keySelector(min);
|
||||||
|
|
||||||
|
foreach (var element in source.Skip(1))
|
||||||
|
{
|
||||||
|
var key = keySelector(element);
|
||||||
|
|
||||||
|
if (comparer.Compare(key, minKey) < 0)
|
||||||
|
{
|
||||||
|
min = element;
|
||||||
|
minKey = key;
|
||||||
|
}
|
||||||
|
else if (comparer.Compare(key, minKey) == 0)
|
||||||
|
{
|
||||||
|
min = conflictResolver(min, element);
|
||||||
|
minKey = keySelector(min);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using Cocotte.Modules.Raids;
|
|
||||||
|
|
||||||
namespace Cocotte.Utils;
|
|
||||||
|
|
||||||
public static class RaidExtensions
|
|
||||||
{
|
|
||||||
public static void AddTestPlayers(this Raid raid)
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user