diff --git a/Modules/AutoHaunt.cs b/Modules/AutoHaunt.cs
index 53cf87a54..5f6172427 100644
--- a/Modules/AutoHaunt.cs
+++ b/Modules/AutoHaunt.cs
@@ -16,7 +16,7 @@ public static class AutoHaunt
return Options.CurrentGameMode switch
{
- CustomGameMode.Standard => validPCs.OrderByDescending(x => x.GetCustomRole() is CustomRoles.Workaholic or CustomRoles.Snitch && x.GetTaskState().RemainingTasksCount <= 2).ThenByDescending(x => x.Is(CustomRoleTypes.Coven)).ThenByDescending(x => x.IsNeutralKiller()).ThenByDescending(x => x.IsImpostor()).ThenByDescending(x => x.GetCustomRole().IsNeutral()).FirstOrDefault(),
+ CustomGameMode.Standard => validPCs.OrderByDescending(x => x.GetCustomRole() is CustomRoles.Workaholic or CustomRoles.Snitch && x.GetTaskState().RemainingTasksCount <= 2).ThenByDescending(x => x.IsNeutralKiller()).ThenByDescending(x => x.IsImpostor()).ThenByDescending(x => x.Is(CustomRoleTypes.Coven)).ThenByDescending(x => x.GetCustomRole().IsNeutral()).FirstOrDefault(),
CustomGameMode.SoloPVP => validPCs.Where(x => x.SoloAlive()).MinBy(x => SoloPVP.GetRankFromScore(x.PlayerId)),
CustomGameMode.FFA => validPCs.MaxBy(x => FreeForAll.KillCount.GetValueOrDefault(x.PlayerId, 0)),
CustomGameMode.StopAndGo => validPCs.MaxBy(x => x.GetTaskState().CompletedTasksCount),
diff --git a/Modules/Camouflage.cs b/Modules/Camouflage.cs
index 206c7e841..40b6719d1 100644
--- a/Modules/Camouflage.cs
+++ b/Modules/Camouflage.cs
@@ -161,7 +161,7 @@ public static class Camouflage
public static void RpcSetSkin(PlayerControl target, bool forceRevert = false, bool revertToDefault = false, bool gameEnd = false, bool revive = false, bool notCommsOrCamo = false, CustomRpcSender sender = null)
{
- if (!AmongUsClient.Instance.AmHost || (!Options.CommsCamouflage.GetBool() && !Camouflager.On && !revive && !notCommsOrCamo) || target == null || (BlockCamouflage && !forceRevert && !revertToDefault && !gameEnd && !revive && !notCommsOrCamo)) return;
+ if (!AmongUsClient.Instance.AmHost || (!Options.CommsCamouflage.GetBool() && !Camouflager.On && !revive && !notCommsOrCamo) || !target || (BlockCamouflage && !forceRevert && !revertToDefault && !gameEnd && !revive && !notCommsOrCamo)) return;
Logger.Info($"New outfit for {target.GetNameWithRole()}", "Camouflage.RpcSetSkin");
@@ -202,7 +202,7 @@ public static class Camouflage
{
var killer = target.GetRealKiller();
- if (Options.AnonymousBodies.GetBool() || target.Is(CustomRoles.Hidden) || (killer != null && killer.Is(CustomRoles.Concealer)))
+ if (Options.AnonymousBodies.GetBool() || target.Is(CustomRoles.Hidden) || (killer && killer.Is(CustomRoles.Concealer)))
newOutfit = new NetworkedPlayerInfo.PlayerOutfit().Set(Translator.GetString("Dead"), 15, "", "", "", "", "");
else if (Options.RemovePetsAtDeadPlayers.GetBool())
newOutfit.PetId = string.Empty;
diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs
index 127b811e6..545e4fd99 100644
--- a/Modules/ExtendedPlayerControl.cs
+++ b/Modules/ExtendedPlayerControl.cs
@@ -2126,7 +2126,7 @@ internal static class ExtendedPlayerControl
public string GetRoleInfo(bool infoLong = false)
{
- CustomRoles role = player.GetCustomRole();
+ CustomRoles role = player.AmOwner && Main.GM.Value ? CustomRoles.GM : player.GetCustomRole();
if (role is CustomRoles.Crewmate or CustomRoles.Impostor) infoLong = false;
string info = (role.IsVanilla() ? "Blurb" : "Info") + (infoLong ? "Long" : string.Empty);
diff --git a/Modules/Extensions/CollectionExtensions.cs b/Modules/Extensions/CollectionExtensions.cs
index 6c7540770..a2b012884 100644
--- a/Modules/Extensions/CollectionExtensions.cs
+++ b/Modules/Extensions/CollectionExtensions.cs
@@ -79,21 +79,6 @@ public static class CollectionExtensions
}
}
- ///
- /// Returns a random element from a collection
- ///
- /// The collection
- /// The type of the collection
- ///
- /// A random element from the collection, or the default value of if the collection is
- /// empty
- ///
- public static T RandomElement(this IReadOnlyList collection)
- {
- if (collection.Count == 0) return default(T);
- return collection[IRandom.Instance.Next(collection.Count)];
- }
-
/// The collection
/// The type of the collection
extension(IEnumerable collection)
@@ -241,6 +226,220 @@ public static class CollectionExtensions
element = default(T);
return false;
}
+
+ ///
+ /// Takes the specified number of random elements from the collection to a List, and yields its elements for enumeration.
+ ///
+ /// The number of random elements to pick.
+ /// A new collection with the specified number of random elements from the original collection.
+ public IEnumerable TakeRandom(int count)
+ {
+ if (collection == null || count <= 0)
+ yield break;
+
+ var reservoir = new List(count);
+ int i = 0;
+
+ foreach (var item in collection)
+ {
+ if (i < count)
+ {
+ reservoir.Add(item);
+ }
+ else
+ {
+ int j = IRandom.Instance.Next(i + 1);
+ if (j < count)
+ reservoir[j] = item;
+ }
+ i++;
+ }
+
+ // yield instead of returning list to avoid extra allocation
+ foreach (var item in reservoir)
+ yield return item;
+ }
+
+ ///
+ /// Takes the specified number of random elements from the collection, and adds them to a new List.
+ ///
+ /// The number of random elements to pick.
+ /// A new List with the specified number of random elements from the original collection.
+ public List TakeRandomToList(int count)
+ {
+ if (collection == null || count <= 0)
+ return [];
+
+ var reservoir = new List(count);
+ int i = 0;
+
+ foreach (var item in collection)
+ {
+ if (i < count)
+ {
+ reservoir.Add(item);
+ }
+ else
+ {
+ int j = IRandom.Instance.Next(i + 1);
+ if (j < count)
+ reservoir[j] = item;
+ }
+ i++;
+ }
+
+ return reservoir;
+ }
+
+ ///
+ /// Partitions a collection into a specified number of parts
+ ///
+ /// The number of parts to partition the collection into
+ /// A collection of collections, each containing a part of the original collection
+ public IEnumerable> Partition(int parts)
+ {
+ List list = collection.ToList();
+ int length = list.Count;
+ if (parts <= 0 || length == 0) yield break;
+
+ if (parts > length) parts = length;
+
+ int size = length / parts;
+ int remainder = length % parts;
+ var index = 0;
+
+ for (var i = 0; i < parts; i++)
+ {
+ int partSize = size + (i < remainder ? 1 : 0);
+ yield return list.Skip(index).Take(partSize);
+ index += partSize;
+ }
+ }
+
+ ///
+ /// Removes an element from a collection
+ ///
+ /// The element to remove
+ ///
+ /// A collection containing all elements of except for
+ ///
+ public IEnumerable Without(T element)
+ {
+ return collection.Where(x => !x.Equals(element));
+ }
+
+ ///
+ /// Shuffles all elements in a collection randomly
+ ///
+ /// A new, shuffled collection as a
+ public List Shuffle()
+ {
+ if (collection is not List list)
+ list = collection.ToList();
+
+ int n = list.Count;
+ var r = IRandom.Instance;
+
+ while (n > 1)
+ {
+ n--;
+ int k = r.Next(n + 1);
+ (list[n], list[k]) = (list[k], list[n]);
+ }
+
+ return list;
+ }
+ }
+
+ /// The collection
+ /// The type of the collection
+ extension(IReadOnlyList collection)
+ {
+ ///
+ /// Partitions a list into a specified number of parts
+ ///
+ /// The number of parts to partition the list into
+ /// A list of lists, each containing a part of the original list
+ public IEnumerable> Partition(int parts)
+ {
+ int length = collection.Count;
+ if (parts <= 0 || length == 0) yield break;
+
+ if (parts > length) parts = length;
+
+ int size = length / parts;
+ int remainder = length % parts;
+ var index = 0;
+
+ for (var i = 0; i < parts; i++)
+ {
+ int partSize = size + (i < remainder ? 1 : 0);
+ yield return collection.Skip(index).Take(partSize);
+ index += partSize;
+ }
+ }
+
+ ///
+ /// Takes the specified number of random elements from the collection.
+ ///
+ /// The number of random elements to pick.
+ /// A new collection with the specified number of random elements from the original collection.
+ public IEnumerable TakeRandom(int count)
+ {
+ if (collection == null || collection.Count == 0 || count <= 0)
+ yield break;
+
+ int n = collection.Count;
+
+ // If asking for >= all elements, just return everything
+ if (count >= n)
+ {
+ for (int i = 0; i < n; i++)
+ yield return collection[i];
+ yield break;
+ }
+
+ // If k is small relative to n → pick unique indices via HashSet
+ // If k is large → invert selection (pick excluded indices instead)
+ if (count <= n / 2)
+ {
+ var chosen = new HashSet();
+ while (chosen.Count < count)
+ {
+ int idx = IRandom.Instance.Next(n);
+ if (chosen.Add(idx))
+ yield return collection[idx];
+ }
+ }
+ else
+ {
+ // More efficient to exclude (n - count) items
+ int excludeCount = n - count;
+ var excluded = new HashSet();
+
+ while (excluded.Count < excludeCount)
+ excluded.Add(IRandom.Instance.Next(n));
+
+ for (int i = 0; i < n; i++)
+ {
+ if (!excluded.Contains(i))
+ yield return collection[i];
+ }
+ }
+ }
+
+ ///
+ /// Returns a random element from a collection
+ ///
+ ///
+ /// A random element from the collection, or the default value of if the collection is
+ /// empty
+ ///
+ public T RandomElement()
+ {
+ if (collection.Count == 0) return default(T);
+ return collection[IRandom.Instance.Next(collection.Count)];
+ }
}
///
@@ -273,7 +472,7 @@ public static class CollectionExtensions
sender.SendMessage(dispose: !hasValue);
}
-
+
#region ToValidPlayers
///
@@ -295,25 +494,11 @@ public static class CollectionExtensions
{
return playerIds.ConvertAll(Utils.GetPlayer).FindAll(x => x);
}
-
+
#endregion
-
+
#region Without
- ///
- /// Removes an element from a collection
- ///
- /// The collection to remove the element from
- /// The element to remove
- /// The type of the elements in the collection
- ///
- /// A collection containing all elements of except for
- ///
- public static IEnumerable Without(this IEnumerable collection, T element)
- {
- return collection.Where(x => !x.Equals(element));
- }
-
///
/// Removes an element from a collection
///
@@ -339,35 +524,11 @@ public static class CollectionExtensions
{
return collection.Where(x => x != element);
}
-
+
#endregion
-
+
#region Shuffle
- ///
- /// Shuffles all elements in a collection randomly
- ///
- /// The type of the collection
- /// The collection to be shuffled
- /// A new, shuffled collection as a
- public static List Shuffle(this IEnumerable collection)
- {
- if (collection is not List list)
- list = collection.ToList();
-
- int n = list.Count;
- var r = IRandom.Instance;
-
- while (n > 1)
- {
- n--;
- int k = r.Next(n + 1);
- (list[n], list[k]) = (list[k], list[n]);
- }
-
- return list;
- }
-
///
/// Shuffles all elements in a collection randomly
///
@@ -410,63 +571,27 @@ public static class CollectionExtensions
return collection;
}
- #endregion
-
- #region Partition
-
///
- /// Partitions a collection into a specified number of parts
+ /// Shuffles all elements in an IList randomly
///
- /// The collection to partition
- /// The number of parts to partition the collection into
- /// The type of the elements in the collection
- /// A collection of collections, each containing a part of the original collection
- public static IEnumerable> Partition(this IEnumerable collection, int parts)
+ /// The IList to be shuffled
+ /// The type of the IList
+ /// The same IList with its elements shuffled
+ public static IList Shuffle(this IList collection)
{
- List list = collection.ToList();
- int length = list.Count;
- if (parts <= 0 || length == 0) yield break;
+ int n = collection.Count;
+ var r = IRandom.Instance;
- if (parts > length) parts = length;
-
- int size = length / parts;
- int remainder = length % parts;
- var index = 0;
-
- for (var i = 0; i < parts; i++)
+ while (n > 1)
{
- int partSize = size + (i < remainder ? 1 : 0);
- yield return list.Skip(index).Take(partSize);
- index += partSize;
+ n--;
+ int k = r.Next(n + 1);
+ (collection[n], collection[k]) = (collection[k], collection[n]);
}
+
+ return collection;
}
-
- ///
- /// Partitions a list into a specified number of parts
- ///
- /// The list to partition
- /// The number of parts to partition the list into
- /// The type of the elements in the list
- /// A list of lists, each containing a part of the original list
- public static IEnumerable> Partition(this IReadOnlyList collection, int parts)
- {
- int length = collection.Count;
- if (parts <= 0 || length == 0) yield break;
-
- if (parts > length) parts = length;
-
- int size = length / parts;
- int remainder = length % parts;
- var index = 0;
-
- for (var i = 0; i < parts; i++)
- {
- int partSize = size + (i < remainder ? 1 : 0);
- yield return collection.Skip(index).Take(partSize);
- index += partSize;
- }
- }
-
+
#endregion
}
diff --git a/Modules/GameState.cs b/Modules/GameState.cs
index 92c2748c3..06fde57ff 100644
--- a/Modules/GameState.cs
+++ b/Modules/GameState.cs
@@ -171,6 +171,8 @@ public class PlayerState(byte playerId)
if (!role.Is(Team.Impostor) && !(role == CustomRoles.Traitor && Traitor.CanGetImpostorOnlyAddons.GetBool()))
SubRoles.ToArray().DoIf(x => x.IsImpOnlyAddon(), RemoveSubRole);
+
+ if (role == CustomRoles.GM) return;
if (role is CustomRoles.Sidekick or CustomRoles.Necromancer or CustomRoles.Deathknight or CustomRoles.Renegade)
SubRoles.ToArray().DoIf(StartGameHostPatch.BasisChangingAddons.ContainsKey, RemoveSubRole);
diff --git a/Modules/Utils.cs b/Modules/Utils.cs
index 1bd036820..e07c401cb 100644
--- a/Modules/Utils.cs
+++ b/Modules/Utils.cs
@@ -4065,7 +4065,7 @@ public static class Utils
Logger.Exception(ex, "AfterPlayerDeathTasks");
}
- if (!target || (Main.DiedThisRound.Contains(target.PlayerId) && IsRevivingRoleAlive()) || Options.CurrentGameMode != CustomGameMode.Standard) return;
+ if (!target || (Main.DiedThisRound.Contains(target.PlayerId) && IsRevivingRoleAlive() && !GameStates.IsMeeting) || Options.CurrentGameMode != CustomGameMode.Standard) return;
if (targetRealKiller)
target.Notify($"<#ffffff>{string.Format(GetString("DeathCommand"), targetRealKiller.PlayerId.ColoredPlayerName(), (targetRealKiller.Is(CustomRoles.Bloodlust) ? $"{CustomRoles.Bloodlust.ToColoredString()} " : string.Empty) + targetRealKiller.GetCustomRole().ToColoredString())}", 10f);
diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs
index 61c8b6a08..798fb05d8 100644
--- a/Patches/ChatCommandPatch.cs
+++ b/Patches/ChatCommandPatch.cs
@@ -1453,7 +1453,7 @@ internal static class ChatCommands
{
var suitableRoles = hnsRoles.FindAll(x => x.Interface.Team == team);
if (suitableRoles.Count == 0) continue;
- allPlayerIds.Shuffle().Where(x => !DraftRoles.ContainsKey(x)).Take(num).Do(x => DraftRoles[x] = [suitableRoles.RandomElement().Role]);
+ allPlayerIds.Where(x => !DraftRoles.ContainsKey(x)).TakeRandom(num).Do(x => DraftRoles[x] = [suitableRoles.RandomElement().Role]);
}
var hiderRoles = hnsRoles.FindAll(x => x.Interface.Team == Team.Crewmate);
@@ -1488,10 +1488,10 @@ internal static class ChatCommands
return;
}
- IEnumerable impRoles = allRoles.Where(x => x.IsImpostor()).Shuffle().Take(Options.FactionMinMaxSettings[Team.Impostor].MaxSetting.GetInt());
- IEnumerable nkRoles = allRoles.Where(x => x.IsNK()).Shuffle().Take(Math.Min(Options.FactionMinMaxSettings[Team.Neutral].MaxSetting.GetInt(), Options.RoleSubCategoryLimits[RoleOptionType.Neutral_Killing][2].GetInt()));
- IEnumerable nnkRoles = allRoles.Where(x => x.IsNonNK()).Shuffle().Take(Math.Min(Options.FactionMinMaxSettings[Team.Neutral].MaxSetting.GetInt() - Options.RoleSubCategoryLimits[RoleOptionType.Neutral_Killing][2].GetInt(), Options.MaxNNKs.GetInt()));
- IEnumerable covenRoles = allRoles.Where(x => x.IsCoven()).Shuffle().Take(Options.FactionMinMaxSettings[Team.Coven].MaxSetting.GetInt());
+ IEnumerable impRoles = allRoles.Where(x => x.IsImpostor()).TakeRandom(Options.FactionMinMaxSettings[Team.Impostor].MaxSetting.GetInt());
+ IEnumerable nkRoles = allRoles.Where(x => x.IsNK()).TakeRandom(Math.Min(Options.FactionMinMaxSettings[Team.Neutral].MaxSetting.GetInt(), Options.RoleSubCategoryLimits[RoleOptionType.Neutral_Killing][2].GetInt()));
+ IEnumerable nnkRoles = allRoles.Where(x => x.IsNonNK()).TakeRandom(Math.Min(Options.FactionMinMaxSettings[Team.Neutral].MaxSetting.GetInt() - Options.RoleSubCategoryLimits[RoleOptionType.Neutral_Killing][2].GetInt(), Options.MaxNNKs.GetInt()));
+ IEnumerable covenRoles = allRoles.Where(x => x.IsCoven()).TakeRandom(Options.FactionMinMaxSettings[Team.Coven].MaxSetting.GetInt());
allRoles.RemoveAll(x => x.IsImpostor());
allRoles.RemoveAll(x => x.IsNK());
@@ -3428,6 +3428,7 @@ internal static class ChatCommands
if (text.StartsWith('/') && !player.IsModdedClient() && (!GameStates.IsMeeting || MeetingHud.Instance.state is not MeetingHud.VoteStates.Results and not MeetingHud.VoteStates.Proceeding))
{
Utils.CheckServerCommand(ref text, out bool spamRequired);
+ if (!spamRequired) canceled = true;
string[] args = text.Split(' ');
foreach (Command command in Command.AllCommands)
@@ -3445,7 +3446,7 @@ internal static class ChatCommands
if (command.AlwaysHidden && spamRequired) Utils.SendMessage("\n", player.PlayerId, GetString("NoSpamAnymoreUseCmd"));
command.Action(player, text, args);
- if (command.IsCanceled) canceled = command.AlwaysHidden || !Options.HostSeesCommandsEnteredByOthers.GetBool();
+ if (command.IsCanceled) canceled |= command.AlwaysHidden || !Options.HostSeesCommandsEnteredByOthers.GetBool();
break;
}
}
diff --git a/Patches/ControlPatch.cs b/Patches/ControlPatch.cs
index 187741233..48ab0730c 100644
--- a/Patches/ControlPatch.cs
+++ b/Patches/ControlPatch.cs
@@ -456,14 +456,25 @@ public static class InGameRoleInfoMenu
MainInfo.DestroyTranslator();
MainInfo.transform.localPosition = new(-2.3f, 0.8f, 4f);
MainInfo.GetComponent().sizeDelta = new(4.5f, 10f);
+
MainInfoTMP.alignment = TextAlignmentOptions.Left;
- MainInfoTMP.fontSize = MainInfoTMP.fontSizeMax = MainInfoTMP.fontSizeMin = 1.75f;
+ MainInfoTMP.enableAutoSizing = true;
+ MainInfoTMP.fontSizeMax = 1.75f;
+ MainInfoTMP.fontSizeMin = 0.75f;
+ MainInfoTMP.enableWordWrapping = true;
+ MainInfoTMP.overflowMode = TextOverflowModes.Overflow;
AddonsInfo = Object.Instantiate(MainInfo, MainInfo.transform.parent);
AddonsInfo.name = "Addons Info";
AddonsInfo.DestroyTranslator();
AddonsInfo.transform.SetLocalX(2.3f);
AddonsInfo.transform.localScale = new(0.7f, 0.7f, 0.7f);
+
+ AddonsInfoTMP.enableAutoSizing = true;
+ AddonsInfoTMP.fontSizeMax = 1.75f;
+ AddonsInfoTMP.fontSizeMin = 0.6f;
+ AddonsInfoTMP.enableWordWrapping = true;
+ AddonsInfoTMP.overflowMode = TextOverflowModes.Overflow;
}
public static void SetRoleInfoRef(PlayerControl player)
@@ -477,26 +488,20 @@ public static class InGameRoleInfoMenu
StringBuilder titleSb = new();
StringBuilder settings = new();
StringBuilder addons = new();
- settings.Append("");
titleSb.Append($"{role.ToColoredString()} {Utils.GetRoleMode(role)}");
- sb.Append("");
sb.Append(player.GetRoleInfo(true).TrimStart());
if (Options.CustomRoleSpawnChances.TryGetValue(role, out StringOptionItem opt)) Utils.ShowChildrenSettings(opt, ref settings, f1: true, disableColor: false);
- settings.Append("");
if (settings.Length > 0) addons.Append($"{settings}\n\n");
- if (role.PetActivatedAbility()) sb.Append($"{GetString("SupportsPetMessage")}");
+ if (role.PetActivatedAbility()) sb.Append($"{GetString("SupportsPetMessage")}");
string searchStr = GetString(role.ToString());
sb.Replace(searchStr, role.ToColoredString());
sb.Replace(searchStr.ToLower(), role.ToColoredString());
- sb.Append("");
List subRoles = Main.PlayerStates[player.PlayerId].SubRoles;
if (subRoles.Count > 0) addons.Append(GetString("AddonListTitle"));
- addons.Append("");
-
subRoles.ForEach(subRole =>
{
addons.Append($"\n\n{subRole.ToColoredString()} {Utils.GetRoleMode(subRole)} {GetString($"{subRole}InfoLong").FixRoleName(subRole)}");
@@ -505,9 +510,8 @@ public static class InGameRoleInfoMenu
addons.Replace(searchSubStr.ToLower(), subRole.ToColoredString());
});
- addons.Append("");
- if (role.UsesPetInsteadOfKill()) sb.Append($"\n\n{GetString("UsesPetInsteadOfKillNotice")}");
- if (player.UsesMeetingShapeshift()) sb.Append($"\n\n{GetString("UsesMeetingShapeshiftNotice")}");
+ if (role.UsesPetInsteadOfKill()) sb.Append($"\n\n{GetString("UsesPetInsteadOfKillNotice")}");
+ if (player.UsesMeetingShapeshift()) sb.Append($"\n\n{GetString("UsesMeetingShapeshiftNotice")}");
sb.Insert(0, $"{titleSb}\n");
diff --git a/Patches/HudPatch.cs b/Patches/HudPatch.cs
index 18870e458..40ac211d2 100644
--- a/Patches/HudPatch.cs
+++ b/Patches/HudPatch.cs
@@ -509,9 +509,9 @@ public static class ButtonCooldownPatch
if (__instance.isCoolingDown)
{
- __instance.cooldownTimerText.text = timer < 10f
+ __instance.cooldownTimerText.text = timer <= 9.9f
? timer.ToString("0.0", NumberFormatInfo.CurrentInfo)
- : ((int)timer).ToString();
+ : timer >= 11 ? ((int)timer).ToString() : "10";
}
}
}
@@ -1021,23 +1021,17 @@ internal static class TaskPanelBehaviourPatch
panel.openPosition = Vector3.Lerp(panel.openPosition, targetOpen, t);
PlayerControl player = PlayerControl.LocalPlayer;
-
string roleInfo = player.GetRoleInfo();
-
var roleWithInfoBuilder = new StringBuilder();
- roleWithInfoBuilder.Append("");
- roleWithInfoBuilder.Append(role.ToColoredString());
- roleWithInfoBuilder.Append(":\r\n");
- roleWithInfoBuilder.Append(roleInfo);
if (Options.CurrentGameMode != CustomGameMode.Standard)
{
string[] splitted = roleInfo.Split(' ');
- roleWithInfoBuilder.Clear();
roleWithInfoBuilder.Append("");
roleWithInfoBuilder.Append(GetString(Options.CurrentGameMode.ToString()));
roleWithInfoBuilder.Append(":\r\n");
+
if (splitted.Length <= 3)
{
roleWithInfoBuilder.Append(roleInfo);
@@ -1053,7 +1047,6 @@ internal static class TaskPanelBehaviourPatch
{
string[] split = roleInfo.Split(' ');
int half = split.Length / 2;
- roleWithInfoBuilder.Clear();
roleWithInfoBuilder.Append("");
roleWithInfoBuilder.Append(role.ToColoredString());
roleWithInfoBuilder.Append(":\r\n");
@@ -1061,6 +1054,13 @@ internal static class TaskPanelBehaviourPatch
roleWithInfoBuilder.Append("\r\n");
roleWithInfoBuilder.Append(string.Join(' ', split[half..]));
}
+ else
+ {
+ roleWithInfoBuilder.Append("");
+ roleWithInfoBuilder.Append(role.ToColoredString());
+ roleWithInfoBuilder.Append(":\r\n");
+ roleWithInfoBuilder.Append(roleInfo);
+ }
StringBuilder finalTextBuilder = new StringBuilder(Utils.ColorString(player.GetRoleColor(), roleWithInfoBuilder.ToString()));
diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs
index 129c2d71f..0620d16ea 100644
--- a/Patches/OnGameStartedPatch.cs
+++ b/Patches/OnGameStartedPatch.cs
@@ -799,7 +799,7 @@ internal static class StartGameHostPatch
roleList.ExceptWith(otherList);
}
- BasisChangingAddons[addon] = roleList.Shuffle().Take(addon.GetCount()).ToList();
+ BasisChangingAddons[addon] = roleList.TakeRandomToList(addon.GetCount());
}
}
}
@@ -1571,7 +1571,7 @@ internal static class StartGameHostPatch
foreach ((byte id, RoleTypes roleTypes) in OverriddenTeamRevealScreen)
{
PlayerControl pc = id.GetPlayer();
- if (pc == null || !pc.IsAlive()) continue;
+ if (!pc || !pc.IsAlive()) continue;
int targetClientId = pc.OwnerId;
if (targetClientId == -1) continue;
diff --git a/Resources/Lang/en_US.jsonc b/Resources/Lang/en_US.jsonc
index 5864f92b2..6bb9a3fc1 100644
--- a/Resources/Lang/en_US.jsonc
+++ b/Resources/Lang/en_US.jsonc
@@ -2637,7 +2637,7 @@
"EndermanSecondsBeforeTP": "Seconds Before Teleporting",
"Evolver": "Evolver",
"EvolverInfo": "Select an Upgrade After Each Meeting",
- "EvolverInfoLong": "(Neutrals):\\nAs the Evolver, you can choose an upgrade after each meeting. Pet to cycle between the available upgrades. The same upgrade can appear multiple times. Once the timer to pick ends, your upgrade will be applied.\\n\\nUpgrades:\\n- Decrease Kill Cooldown (by 2.5s)\\n- Gain Impostor Vision (unaffected by lights sabotage)\\n- Increase Vision (by 0.4x)\\n- Increase Speed (by 0.25x)\\n- Increase Kill Distance (Short → Medium → Long)\\n- Gain Venting Ability (and 1 use)\\n- Increase Venting Use Limit (by 3)\\n- Gain Sabotage Ability (and 1 use)\\n- Increase Sabotage Use Limit (by 2)\\n- Gain Shield (cannot be killed in the current round)\\n\\nRemarks:\\nIf you can vent, the number of vent uses you have is displayed next to your role name like this: (V: 1) - Same with the sabotage use limit: (S: 2)",
+ "EvolverInfoLong": "(Neutrals):\\nAs the Evolver, you can choose an upgrade after each meeting. Pet to cycle between the available upgrades. The same upgrade can appear multiple times. Once the timer to pick ends, your upgrade will be applied.\\n\\nUpgrades:\\n- Decrease Kill Cooldown (by 5s)\\n- Gain Impostor Vision (unaffected by lights sabotage)\\n- Increase Vision (by 0.6x)\\n- Increase Speed (by 0.5x)\\n- Increase Kill Distance (Short → Medium → Long)\\n- Gain Venting Ability (and 3 uses)\\n- Increase Venting Use Limit (by 10)\\n- Gain Sabotage Ability (and 2 uses)\\n- Increase Sabotage Use Limit (by 5)\\n- Gain Shield (cannot be killed in the current round)\\n\\nRemarks:\\nIf you can vent, the number of vent uses you have is displayed next to your role name like this: (V: 1) - Same with the sabotage use limit: (S: 2)",
"EvolverProgress.Sabotage": " (S: {0})",
"EvolverProgress.Vent": " (V: {0})",
"EvolverSuffix": "Selected upgrade will be applied in <#00ffa5>{0}s\\nSelected upgrade: <#00ffa5>{1} (Pet to cycle)\\nAvailable upgrades: <#00ffa5>{2}",
@@ -2690,7 +2690,7 @@
"GaslighterWinCondition.LastStanding": "<#ffab1b>Last one standing",
"Glitch": "The Glitch",
"GlitchInfo": "Hack and kill everyone",
- "GlitchInfoLong": "(Neutrals):\\nAs the Glitch, your goal is to kill everyone on the ship. You can hack players to prevent them from killing, venting, sabotaging, and reporting dead bodies for a limited time. You can also disguise as a random player for a limited time. Additionally, your cooldowns go down while in vents.",
+ "GlitchInfoLong": "(Neutrals):\\nAs the Glitch, your goal is to kill everyone on the ship. You can hack players to prevent them from killing, venting, sabotaging, and reporting dead bodies for a limited time by single clicking on them with your kill button. Double click with it to kill them normally. You can also disguise by shapeshifting for a limited time. Additionally, your cooldowns go down while in vents.",
"GlitchCanVote": "Can vote",
"God": "God",
"GodInfo": "Everything is under your control",
diff --git a/Roles/HideAndSeek/Locator.cs b/Roles/HideAndSeek/Locator.cs
index 1840c494e..99f47fa6e 100644
--- a/Roles/HideAndSeek/Locator.cs
+++ b/Roles/HideAndSeek/Locator.cs
@@ -71,11 +71,11 @@ public class Locator : RoleBase, IHideAndSeekRole
void OnElapsed()
{
PlayerControl pc = Utils.GetPlayerById(playerId);
- if (pc == null || !pc.IsAlive()) return;
+ if (!pc || !pc.IsAlive()) return;
- PlayerControl target = CustomHnS.PlayerRoles.Where(x => x.Value.Interface.Team != Team.Impostor).Select(x => Utils.GetPlayerById(x.Key)).Where(x => x != null && x.IsAlive()).Shuffle().FirstOrDefault();
+ PlayerControl target = CustomHnS.PlayerRoles.Where(x => x.Value.Interface.Team != Team.Impostor).Select(x => Utils.GetPlayerById(x.Key)).Where(x => x && x.IsAlive()).RandomElement();
- if (target != null)
+ if (target)
{
TargetId = target.PlayerId;
TargetArrow.Add(pc.PlayerId, target.PlayerId);
diff --git a/Roles/Standard/Impostor/Concealing/Kidnapper.cs b/Roles/Standard/Impostor/Concealing/Kidnapper.cs
index de14f243c..6c729674d 100644
--- a/Roles/Standard/Impostor/Concealing/Kidnapper.cs
+++ b/Roles/Standard/Impostor/Concealing/Kidnapper.cs
@@ -7,9 +7,12 @@ internal class Kidnapper : RoleBase
{
public static bool On;
- public static OptionItem KCD;
- public static OptionItem SSCD;
- private static int Id => 643300;
+ private static OptionItem KCD;
+ private static OptionItem SSCD;
+ private static OptionItem AbilityUseLimit;
+ private static OptionItem AbilityUseGainWithEachKill;
+
+ private const int Id = 643300;
public override bool IsEnable => On;
public override void SetupCustomOption()
@@ -23,6 +26,14 @@ internal class Kidnapper : RoleBase
KCD = new FloatOptionItem(Id + 3, "KillCooldown", new(0f, 180f, 0.5f), 30f, TabGroup.ImpostorRoles)
.SetParent(Options.CustomRoleSpawnChances[CustomRoles.Kidnapper])
.SetValueFormat(OptionFormat.Seconds);
+
+ AbilityUseLimit = new IntegerOptionItem(Id + 4, "AbilityUseLimit", new(0, 20, 1), 1, TabGroup.ImpostorRoles)
+ .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Kidnapper])
+ .SetValueFormat(OptionFormat.Times);
+
+ AbilityUseGainWithEachKill = new FloatOptionItem(Id + 5, "AbilityUseGainWithEachKill", new(0f, 5f, 0.1f), 0.5f, TabGroup.ImpostorRoles)
+ .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Kidnapper])
+ .SetValueFormat(OptionFormat.Times);
}
public override void ApplyGameOptions(IGameOptions opt, byte playerId)
@@ -40,8 +51,12 @@ internal class Kidnapper : RoleBase
{
if (!kidnapper || !target || !shapeshifting) return true;
+ if (kidnapper.GetAbilityUseLimit() < 1f) return false;
+
if (!target.TP(kidnapper))
kidnapper.Notify(Utils.ColorString(Color.yellow, Translator.GetString("TargetCannotBeTeleported")));
+ else
+ kidnapper.RpcRemoveAbilityUse();
return false;
}
@@ -54,5 +69,6 @@ internal class Kidnapper : RoleBase
public override void Add(byte playerId)
{
On = true;
+ playerId.SetAbilityUseLimit(AbilityUseLimit.GetFloat());
}
}
\ No newline at end of file
diff --git a/Roles/Standard/Impostor/Support/Centralizer.cs b/Roles/Standard/Impostor/Support/Centralizer.cs
index 610b2b997..ca18224c1 100644
--- a/Roles/Standard/Impostor/Support/Centralizer.cs
+++ b/Roles/Standard/Impostor/Support/Centralizer.cs
@@ -80,7 +80,7 @@ public class Centralizer : RoleBase
if (MarkedPosition.HasValue)
{
if (pc.GetAbilityUseLimit() < 1) return;
- Main.EnumerateAlivePlayerControls().Shuffle().Take(NumPlayersTeleported.GetInt()).MassTP(MarkedPosition.Value);
+ Main.EnumerateAlivePlayerControls().TakeRandom(NumPlayersTeleported.GetInt()).MassTP(MarkedPosition.Value);
MarkedPosition = null;
pc.RpcRemoveAbilityUse();
}
diff --git a/Roles/Standard/Impostor/Support/EvilEraser.cs b/Roles/Standard/Impostor/Support/EvilEraser.cs
index 29386c0c2..6bf66fcb7 100644
--- a/Roles/Standard/Impostor/Support/EvilEraser.cs
+++ b/Roles/Standard/Impostor/Support/EvilEraser.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using Hazel;
using static EHR.Translator;
namespace EHR.Roles;
@@ -149,12 +148,12 @@ internal class EvilEraser : RoleBase
PlayerToErase.ForEach(id =>
{
PlayerControl pc = Utils.GetPlayerById(id);
- if (pc == null || !pc.IsAlive() || pc.Is(CustomRoles.Bloodlust)) return;
+ if (!pc || !pc.IsAlive() || pc.Is(CustomRoles.Bloodlust)) return;
CustomRoles erasedRole = pc.IsImpostor() ? CustomRoles.ImpostorEHR : pc.IsCrewmate() ? CustomRoles.CrewmateEHR : pc.Is(Team.Coven) ? CustomRoles.CovenMember : CustomRoles.Amnesiac;
pc.RpcSetCustomRole(erasedRole);
pc.RpcChangeRoleBasis(erasedRole);
- pc.Notify(GetString("LostRoleByEraser"));
+ pc.Notify(GetString("LostRoleByEvilEraser"));
Logger.Info($"{pc.GetNameWithRole().RemoveHtmlTags()} lost their role", "Eraser");
ErasedPlayers.Add(id);
});
diff --git a/Roles/Standard/Neutral/Killing/Evolver.cs b/Roles/Standard/Neutral/Killing/Evolver.cs
index f8908a92b..a114642e5 100644
--- a/Roles/Standard/Neutral/Killing/Evolver.cs
+++ b/Roles/Standard/Neutral/Killing/Evolver.cs
@@ -117,7 +117,7 @@ public class Evolver : RoleBase
{
if (!EvolverPC.IsAlive()) return;
- Upgrades = Enum.GetValues().Except(GetBannedUpgradeList()).Shuffle().GetRange(0, 3);
+ Upgrades = Enum.GetValues().Except(GetBannedUpgradeList()).TakeRandomToList(3);
SelectedUpgradeIndex = 0;
ChooseTimer = 15;
@@ -130,15 +130,10 @@ public class Evolver : RoleBase
{
var banned = new List();
if (Stats.KillCooldown <= Limits.MinKillCooldown) banned.Add(Upgrade.DecreaseKillCooldown);
-
if (Stats.ImpostorVision) banned.Add(Upgrade.GainImpostorVision);
-
if (Stats.Vision >= Limits.MaxVision) banned.Add(Upgrade.IncreaseVision);
-
if (Stats.Speed >= Limits.MaxSpeed) banned.Add(Upgrade.IncreaseSpeed);
-
if (Stats.KillDistance >= Limits.MaxKillDistance) banned.Add(Upgrade.IncreaseKillDistance);
-
banned.Add(Stats.CanVent ? Upgrade.GainVent : Upgrade.IncreaseVentUseLimit);
banned.Add(Stats.CanSabotage ? Upgrade.GainSabotage : Upgrade.IncreaseSabotageUseLimit);
return banned;
@@ -165,33 +160,33 @@ public class Evolver : RoleBase
switch (Upgrades[SelectedUpgradeIndex])
{
case Upgrade.DecreaseKillCooldown:
- Stats.KillCooldown -= 2.5f;
+ Stats.KillCooldown -= 5f;
break;
case Upgrade.GainImpostorVision:
Stats.ImpostorVision = true;
break;
case Upgrade.IncreaseVision:
- Stats.Vision += 0.4f;
+ Stats.Vision += 0.6f;
break;
case Upgrade.IncreaseSpeed:
- Stats.Speed += 0.25f;
+ Stats.Speed += 0.5f;
break;
case Upgrade.IncreaseKillDistance:
Stats.KillDistance++;
break;
case Upgrade.GainVent:
Stats.CanVent = true;
- Stats.VentUseLimit = 1;
+ Stats.VentUseLimit = 3;
break;
case Upgrade.IncreaseVentUseLimit:
- Stats.VentUseLimit += 3;
+ Stats.VentUseLimit += 10;
break;
case Upgrade.GainSabotage:
Stats.CanSabotage = true;
- Stats.SabotageUseLimit = 1;
+ Stats.SabotageUseLimit = 2;
break;
case Upgrade.IncreaseSabotageUseLimit:
- Stats.SabotageUseLimit += 2;
+ Stats.SabotageUseLimit += 5;
break;
case Upgrade.GainShield:
Stats.Shielded = true;
diff --git a/Roles/Standard/Neutral/Killing/QuizMaster.cs b/Roles/Standard/Neutral/Killing/QuizMaster.cs
index a02e336d0..75df16b0b 100644
--- a/Roles/Standard/Neutral/Killing/QuizMaster.cs
+++ b/Roles/Standard/Neutral/Killing/QuizMaster.cs
@@ -244,16 +244,16 @@ internal class QuizMaster : RoleBase
(IEnumerable WrongAnswers, string CorrectAnswer) answers = index switch
{
1 when abc => (Enum.GetValues().Skip(1).Where(x => x != Data.LastReportedPlayer.Player.GetTeam()).Select(x => Translator.GetString($"{x}")), Translator.GetString($"{Data.LastReportedPlayer.Player.GetTeam()}")),
- 1 => (AllColors.Without(Data.LastReportedPlayer.ColorString).Shuffle().Take(2), Data.LastReportedPlayer.ColorString),
+ 1 => (AllColors.Without(Data.LastReportedPlayer.ColorString).TakeRandom(2), Data.LastReportedPlayer.ColorString),
2 when abc => (new[] { PlayerState.DeathReason.Suicide, PlayerState.DeathReason.Kill, PlayerState.DeathReason.etc }.Without(Main.PlayerStates[Data.LastReportedPlayer.Player!.PlayerId].deathReason).Select(x => Translator.GetString($"DeathReason.{x}")), Translator.GetString($"DeathReason.{Main.PlayerStates[Data.LastReportedPlayer.Player!.PlayerId].deathReason}")),
2 => (GetTwoRandomNames(Data.LastPlayerPressedButtonName), Data.LastPlayerPressedButtonName),
3 when abc => (["Town Of Us", "Town Of Host Enhanced"], "Endless Host Roles"),
3 => (AllSabotages.Take(2).Select(x => Translator.GetString($"{x}")), Translator.GetString($"{Data.LastSabotage}")),
4 when abc => (["tukasa0001", "0xDrMoe"], "Gurge44"),
- 4 => (CustomRoleSelector.RoleResult.Values.Without(Main.LastVotedPlayerInfo!.Object.GetCustomRole()).Shuffle().Take(2).Select(x => x.ToColoredString()), Main.LastVotedPlayerInfo!.Object.GetCustomRole().ToColoredString()),
+ 4 => (CustomRoleSelector.RoleResult.Values.Without(Main.LastVotedPlayerInfo!.Object.GetCustomRole()).TakeRandom(2).Select(x => x.ToColoredString()), Main.LastVotedPlayerInfo!.Object.GetCustomRole().ToColoredString()),
5 when abc => (Enum.GetValues().Skip(1).Where(x => x != randomRole.GetTeam()).Select(x => Translator.GetString($"{x}")), Translator.GetString($"{randomRole.GetTeam()}")),
5 => (GetTwoRandomNames(Data.LastReporterName), Data.LastReporterName),
- 6 when abc => (Enum.GetValues().Where(x => x != randomRole.GetCustomRoleTypes()).Shuffle().Take(2).Select(x => Translator.GetString($"{x}")), Translator.GetString($"{randomRole.GetCustomRoleTypes()}")),
+ 6 when abc => (Enum.GetValues().Where(x => x != randomRole.GetCustomRoleTypes()).TakeRandom(2).Select(x => Translator.GetString($"{x}")), Translator.GetString($"{randomRole.GetCustomRoleTypes()}")),
6 => (GetTwoRandomNumbers(Data.NumPlayersVotedLastMeeting, 0, 15), Data.NumPlayersVotedLastMeeting.ToString()),
7 => (GetTwoRandomNumbers(Data.NumMeetings, Math.Max(Data.NumMeetings - 3, 0), Data.NumMeetings + 3), Data.NumMeetings.ToString()),
8 => (GetTwoRandomNames(Data.FirstReportedBodyPlayerName), Data.FirstReportedBodyPlayerName),
@@ -262,7 +262,7 @@ internal class QuizMaster : RoleBase
11 => (GetTwoRandomNumbers(Data.NumPlayersDeadFirstRound, Math.Max(Data.NumPlayersDeadFirstRound - 3, 0), Data.NumPlayersDeadFirstRound + 3), Data.NumPlayersDeadFirstRound.ToString()),
12 => (GetTwoRandomNumbers(Data.NumSabotages, Math.Max(Data.NumSabotages - 3, 0), Data.NumSabotages + 3), Data.NumSabotages.ToString()),
13 => (GetTwoRandomNumbers(GameData.Instance.CompletedTasks, 0, GameData.Instance.TotalTasks), GameData.Instance.CompletedTasks.ToString()),
- 14 => (Enum.GetValues().Without(RoleTypes.CrewmateGhost).Without(RoleTypes.ImpostorGhost).Where(x => x != randomRole.GetRoleTypes()).Shuffle().Take(2).Select(x => Translator.GetString($"{x}")), Translator.GetString($"{randomRole.GetRoleTypes()}")),
+ 14 => (Enum.GetValues().Without(RoleTypes.CrewmateGhost).Without(RoleTypes.ImpostorGhost).Where(x => x != randomRole.GetRoleTypes()).TakeRandom(2).Select(x => Translator.GetString($"{x}")), Translator.GetString($"{randomRole.GetRoleTypes()}")),
_ => ([], string.Empty)
};
@@ -272,7 +272,7 @@ internal class QuizMaster : RoleBase
return new(title, allAnswersList.ToArray(), correctIndex);
- IEnumerable GetTwoRandomNames(string except) => Main.EnumeratePlayerControls().Select(x => x?.GetRealName()).Without(except).Shuffle().TakeLast(2);
+ IEnumerable GetTwoRandomNames(string except) => Main.EnumeratePlayerControls().Select(x => x?.GetRealName()).Without(except).TakeRandom(2);
IEnumerable GetTwoRandomNumbers(params int[] nums) => IRandom.SequenceUnique(3, nums[1], nums[2] + 1).Without(nums[0]).Take(2).Select(x => x.ToString());
}
diff --git a/Roles/Standard/Neutral/Killing/Thanos.cs b/Roles/Standard/Neutral/Killing/Thanos.cs
index f46c8dd15..d48718d5d 100644
--- a/Roles/Standard/Neutral/Killing/Thanos.cs
+++ b/Roles/Standard/Neutral/Killing/Thanos.cs
@@ -56,7 +56,7 @@ public class Thanos : RoleBase
CollectedStones = [];
ActiveStone = null;
MindStoneUsed = false;
- PlayersWithStones = Main.EnumerateAlivePlayerControls().Shuffle().Take(Enum.GetValues().Length).Select(x => x.PlayerId).ToList();
+ PlayersWithStones = Main.EnumerateAlivePlayerControls().Select(x => x.PlayerId).TakeRandomToList(Enum.GetValues().Length);
ThanosId = playerId;
Instances.Add(this);
}