diff --git a/CTA/CustomTeamAssigner/CustomRoles.cs b/CTA/CustomTeamAssigner/CustomRoles.cs index b6e1e7924..146101c82 100644 --- a/CTA/CustomTeamAssigner/CustomRoles.cs +++ b/CTA/CustomTeamAssigner/CustomRoles.cs @@ -91,6 +91,7 @@ public enum CustomRoles Silencer, Sniper, ImperiusCurse, // Soul Catcher + Stasis, Swapster, Swiftclaw, Swooper, @@ -261,6 +262,7 @@ public enum CustomRoles Agitater, Amnesiac, + Amogus, Arsonist, Backstabber, Bandit, diff --git a/CustomRoles.cs b/CustomRoles.cs index ce22f98a2..146101c82 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -91,6 +91,7 @@ public enum CustomRoles Silencer, Sniper, ImperiusCurse, // Soul Catcher + Stasis, Swapster, Swiftclaw, Swooper, diff --git a/GameMode/RoomRush.cs b/GameMode/RoomRush.cs index dd72b90db..64f7fa2bb 100644 --- a/GameMode/RoomRush.cs +++ b/GameMode/RoomRush.cs @@ -148,7 +148,7 @@ public static class RoomRush .SetColor(color) .SetGameMode(gameMode); - DisplayArrowToRoom = new BooleanOptionItem(id, "RR_DisplayArrowToRoom", false, TabGroup.GameSettings) + DisplayArrowToRoom = new BooleanOptionItem(id++, "RR_DisplayArrowToRoom", false, TabGroup.GameSettings) .SetColor(color) .SetGameMode(gameMode); diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 71580452a..4717df31c 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -132,6 +132,7 @@ internal static class CustomRolesHelper CustomRoles.Inhibitor => CustomRoles.Impostor, CustomRoles.Occultist => CustomRoles.Impostor, CustomRoles.Kidnapper => CustomRoles.Shapeshifter, + CustomRoles.Stasis => UsePets ? CustomRoles.Impostor : CustomRoles.Shapeshifter, CustomRoles.Wasp => CustomRoles.Impostor, CustomRoles.Assumer => CustomRoles.Impostor, CustomRoles.Augmenter => CustomRoles.Shapeshifter, @@ -629,6 +630,7 @@ internal static class CustomRolesHelper CustomRoles.Augmenter or CustomRoles.Inhibitor or CustomRoles.Kidnapper or + CustomRoles.Stasis or CustomRoles.Occultist or CustomRoles.Wasp or CustomRoles.Hypnotist or @@ -918,6 +920,7 @@ internal static class CustomRolesHelper { return role is CustomRoles.Wizard or + CustomRoles.Stasis or CustomRoles.Occultist or CustomRoles.Warlock or CustomRoles.Swiftclaw or @@ -1485,6 +1488,7 @@ internal static class CustomRolesHelper CustomRoles.RiftMaker => RoleOptionType.Impostor_Concealing, CustomRoles.Scavenger => RoleOptionType.Impostor_Concealing, CustomRoles.ImperiusCurse => RoleOptionType.Impostor_Concealing, + CustomRoles.Stasis => RoleOptionType.Impostor_Concealing, CustomRoles.Swiftclaw => RoleOptionType.Impostor_Concealing, CustomRoles.Swooper => RoleOptionType.Impostor_Concealing, CustomRoles.Stealth => RoleOptionType.Impostor_Concealing, @@ -1748,4 +1752,4 @@ public enum CountTypes Arsonist, Cherokious, Sheriff -} +} \ No newline at end of file diff --git a/Modules/ErrorText.cs b/Modules/ErrorText.cs index a93361a54..f23e38c7b 100644 --- a/Modules/ErrorText.cs +++ b/Modules/ErrorText.cs @@ -39,9 +39,7 @@ public class ErrorText : MonoBehaviour public void LateUpdate() { if (!Text.enabled) return; - if (!Camera) Camera = !HudManager.InstanceExists ? _camera : _camera1; - if (Camera) transform.position = AspectPosition.ComputeWorldPosition(Camera, AspectPosition.EdgeAlignments.Top, TextOffset); } @@ -107,10 +105,7 @@ public class ErrorText : MonoBehaviour Text.text = text; } catch (NullReferenceException) { } - catch (Exception e) - { - Logger.Error(e.ToString(), "ErrorText.UpdateText"); - } + catch (Exception e) { Logger.Error(e.ToString(), "ErrorText.UpdateText"); } } public void Clear() diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 0f8f28ab8..631f3eee7 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -1804,14 +1804,11 @@ internal static class ExtendedPlayerControl List subRoles = target.GetCustomSubRoles(); if (subRoles.Contains(CustomRoles.Bloodlust) || target.IsConverted()) return Team.Neutral; - if (subRoles.Contains(CustomRoles.Madmate)) return Team.Impostor; CustomRoles role = target.GetCustomRole(); if (role.IsImpostorTeamV2()) return Team.Impostor; - if (role.IsNeutralTeamV2()) return Team.Neutral; - return role.IsCrewmateTeamV2() ? Team.Crewmate : Team.None; } diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index f87f76e97..1ed145e78 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -400,15 +400,18 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS AURoleOptions.ShapeshifterDuration = 1f; } - if (Options.UsePhantomBasis.GetBool() && role.SimpleAbilityTrigger()) AURoleOptions.PhantomDuration = 1f; + if (Options.UsePhantomBasis.GetBool() && role.SimpleAbilityTrigger()) + AURoleOptions.PhantomDuration = 1f; - if ((Options.UseUnshiftTrigger.GetBool() || role.AlwaysUsesUnshift()) && role.SimpleAbilityTrigger()) AURoleOptions.ShapeshifterDuration = 0f; + if ((Options.UseUnshiftTrigger.GetBool() || role.AlwaysUsesUnshift()) && role.SimpleAbilityTrigger()) + AURoleOptions.ShapeshifterDuration = 0f; // =================================================================================================================== AURoleOptions.EngineerCooldown = Mathf.Max(0.01f, AURoleOptions.EngineerCooldown); - if (Main.AllPlayerKillCooldown.TryGetValue(player.PlayerId, out float killCooldown)) AURoleOptions.KillCooldown = Mathf.Max(0.01f, killCooldown); + if (Main.AllPlayerKillCooldown.TryGetValue(player.PlayerId, out float killCooldown)) + AURoleOptions.KillCooldown = Mathf.Max(0.01f, killCooldown); if (Main.AllPlayerSpeed.TryGetValue(player.PlayerId, out float speed)) { @@ -428,7 +431,8 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS Options.AdditionalEmergencyCooldownTime.GetInt()); } - if (Options.SyncButtonMode.GetBool() && Options.SyncedButtonCount.GetValue() <= Options.UsedButtonCount) opt.SetInt(Int32OptionNames.EmergencyCooldown, 3600); + if (Options.SyncButtonMode.GetBool() && Options.SyncedButtonCount.GetValue() <= Options.UsedButtonCount) + opt.SetInt(Int32OptionNames.EmergencyCooldown, 3600); MeetingTimeManager.ApplyGameOptions(opt); @@ -456,8 +460,7 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS } catch (Exception e) { - Logger.Fatal($"Error for {player.GetRealName()} ({player.GetCustomRole()}): {e}", "PlayerGameOptionsSender.BuildGameOptions"); - Logger.SendInGame($"Error syncing settings for {player.GetRealName()} - Please report this bug to the developer AND SEND LOGS"); + Logger.Error($"Error for {player.GetRealName()} ({player.GetCustomRole()}): {e}", "PlayerGameOptionsSender.BuildGameOptions"); return BasedGameOptions; } } diff --git a/Modules/ModUpdater.cs b/Modules/ModUpdater.cs index 7f88d27a6..86fdc0e46 100644 --- a/Modules/ModUpdater.cs +++ b/Modules/ModUpdater.cs @@ -41,7 +41,7 @@ public static class ModUpdater if (!IsChecked) { bool done = CheckReleaseFromGithub(Main.BetaBuildUrl.Value != "").GetAwaiter().GetResult(); - Logger.Warn("done: " + done, "CheckRelease"); + Logger.Msg("done: " + done, "CheckRelease"); Logger.Info("hasupdate: " + HasUpdate, "CheckRelease"); Logger.Info("forceupdate: " + ForceUpdate, "CheckRelease"); Logger.Info("downloadUrl: " + DownloadUrl, "CheckRelease"); @@ -61,17 +61,14 @@ public static class ModUpdater using StreamReader reader = new(stream); result = reader.ReadToEnd(); } - finally - { - stream.Close(); - } + finally { stream.Close(); } return result; } public static async Task CheckReleaseFromGithub(bool beta = false) { - Logger.Warn("Checking GitHub Release", "CheckRelease"); + Logger.Msg("Checking GitHub Release", "CheckRelease"); const string url = URLGithub + "/releases/latest"; try @@ -191,10 +188,7 @@ public static class ModUpdater } } } - catch (Exception e) - { - Logger.Error($"Failed to clear update residue\n{e}", "DeleteOldFiles"); - } + catch (Exception e) { Logger.Error($"Failed to clear update residue\n{e}", "DeleteOldFiles"); } } } diff --git a/Modules/Statistics.cs b/Modules/Statistics.cs index df2a65fc2..3e9430fc6 100644 --- a/Modules/Statistics.cs +++ b/Modules/Statistics.cs @@ -148,6 +148,11 @@ public static class Statistics if (correctGuesses >= 2) Achievements.Type.BeginnerGuesser.CompleteAfterGameEnd(); if (correctGuesses >= 4) Achievements.Type.GuessMaster.CompleteAfterGameEnd(); if (correctGuesses >= 6) Achievements.Type.BestGuesserAward.CompleteAfterGameEnd(); + + + Main.NumWinsPerGM.TryAdd(Options.CurrentGameMode, []); + Main.NumWinsPerGM[Options.CurrentGameMode].AddRange(CustomWinnerHolder.WinnerIds.ToValidPlayers().ToDictionary(x => x.FriendCode, _ => 0), overrideExistingKeys: false); + Main.NumWinsPerGM[Options.CurrentGameMode].AdjustAllValues(x => ++x); } catch (Exception e) { Utils.ThrowException(e); } diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 9fc2cd147..36c5a461c 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -1360,11 +1360,11 @@ public static class Utils } } - public static void ShowLastRoles(byte PlayerId = byte.MaxValue) + public static void ShowLastRoles(byte playerId = byte.MaxValue) { if (AmongUsClient.Instance.IsGameStarted) { - SendMessage(GetString("CantUse.lastroles"), PlayerId); + SendMessage(GetString("CantUse.lastroles"), playerId); return; } @@ -1460,25 +1460,25 @@ public static class Utils sb.Append(""); - SendMessage("\n", PlayerId, sb.ToString()); + SendMessage("\n", playerId, sb.ToString()); } - public static void ShowKillLog(byte PlayerId = byte.MaxValue) + public static void ShowKillLog(byte playerId = byte.MaxValue) { if (GameStates.IsInGame) { - SendMessage(GetString("CantUse.killlog"), PlayerId); + SendMessage(GetString("CantUse.killlog"), playerId); return; } - if (EndGamePatch.KillLog != string.Empty) SendMessage(EndGamePatch.KillLog, PlayerId); + if (EndGamePatch.KillLog != string.Empty) SendMessage(EndGamePatch.KillLog, playerId); } - public static void ShowLastResult(byte PlayerId = byte.MaxValue) + public static void ShowLastResult(byte playerId = byte.MaxValue) { if (GameStates.IsInGame) { - SendMessage(GetString("CantUse.lastresult"), PlayerId); + SendMessage(GetString("CantUse.lastresult"), playerId); return; } @@ -1486,24 +1486,38 @@ public static class Utils StringBuilder sb = new(); if (SetEverythingUpPatch.LastWinsText != string.Empty) sb.Append($"{GetString("LastResult")} {SetEverythingUpPatch.LastWinsText}"); - if (SetEverythingUpPatch.LastWinsReason != string.Empty) sb.Append($"\n{GetString("LastEndReason")} {SetEverythingUpPatch.LastWinsReason}"); - - if (sb.Length > 0) SendMessage("\n", PlayerId, sb.ToString()); + if (sb.Length > 0) SendMessage("\n", playerId, sb.ToString()); } - public static void ShowLastAddOns(byte PlayerId = byte.MaxValue) + public static void ShowLastAddOns(byte playerId = byte.MaxValue) { if (GameStates.IsInGame) { - SendMessage(GetString("CantUse.lastresult"), PlayerId); + SendMessage(GetString("CantUse.lastresult"), playerId); return; } if (!CustomGameMode.Standard.IsActiveOrIntegrated()) return; string result = Main.LastAddOns.Values.Join(delimiter: "\n"); - SendMessage("\n", PlayerId, result); + SendMessage("\n", playerId, result); + } + + public static string GetWinCountsString() + { + if (!Main.NumWinsPerGM.TryGetValue(Options.CurrentGameMode, out var dictionary) || dictionary.Count == 0) return string.Empty; + + StringBuilder sb = new(); + sb.AppendLine($"<#ffffff>{GetString("WinsCountTitle")}:"); + + foreach ((string friendcode, int wins) in dictionary) + { + if (!Main.AllAlivePlayerControls.FindFirst(x => x.FriendCode == friendcode, out var player)) continue; + sb.AppendLine($"{player.PlayerId.ColoredPlayerName()}: {wins}"); + } + + return sb.ToString().Trim(); } public static string GetSubRolesText(byte id, bool disableColor = false, bool intro = false, bool summary = false) @@ -2644,16 +2658,17 @@ public static class Utils CustomRoles.Twister => Twister.ShapeshiftCooldown.GetInt(), CustomRoles.Abyssbringer => Abyssbringer.BlackHolePlaceCooldown.GetInt(), CustomRoles.Warlock => Warlock.IsCursed ? -1 : Warlock.ShapeshiftCooldown.GetInt(), + CustomRoles.Stasis => Stasis.AbilityCooldown.GetInt() + (includeDuration ? Stasis.AbilityDuration.GetInt() : 0), CustomRoles.Swiftclaw => Swiftclaw.DashCD.GetInt() + (includeDuration ? Swiftclaw.DashDuration.GetInt() : 0), CustomRoles.Hypnotist => Hypnotist.AbilityCooldown.GetInt() + (includeDuration ? Hypnotist.AbilityDuration.GetInt() : 0), CustomRoles.Parasite => (int)Parasite.SSCD + (includeDuration ? (int)Parasite.SSDur : 0), CustomRoles.Tiger => Tiger.EnrageCooldown.GetInt() + (includeDuration ? Tiger.EnrageDuration.GetInt() : 0), CustomRoles.Nonplus => Nonplus.BlindCooldown.GetInt() + (includeDuration ? Nonplus.BlindDuration.GetInt() : 0), + CustomRoles.Amogus => Amogus.AbilityCooldown.GetInt() + (includeDuration ? Amogus.AbilityDuration.GetInt() : 0), CustomRoles.Cherokious => Cherokious.KillCooldown.GetInt(), CustomRoles.Shifter => Shifter.KillCooldown.GetInt(), CustomRoles.NoteKiller => NoteKiller.AbilityCooldown.GetInt(), CustomRoles.Weatherman => Weatherman.AbilityCooldown.GetInt(), - CustomRoles.Amogus => Amogus.AbilityCooldown.GetInt(), _ => -1 }; @@ -2882,8 +2897,8 @@ public static class Utils TargetDies(target.GetRealKiller(), target); } - - if (!onMeeting) + + if (!onMeeting) Amogus.OnAnyoneDead(target); Adventurer.OnAnyoneDead(target); diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 40402dff2..541c43d15 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -103,7 +103,7 @@ internal static class ChatCommands public static Dictionary> DraftRoles = []; public static Dictionary DraftResult = []; - + public static readonly HashSet Spectators = []; public static readonly HashSet LastSpectators = []; @@ -341,12 +341,18 @@ internal static class ChatCommands private static void SpectateCommand(ChatController __instance, PlayerControl player, string text, string[] args) { + if (Options.DisableSpectateCommand.GetBool()) + { + Utils.SendMessage("\n", player.PlayerId, GetString("SpectateDisabled")); + return; + } + if (!AmongUsClient.Instance.AmHost) { RequestCommandProcessingFromHost(nameof(SpectateCommand), text); return; } - + if (LastSpectators.Contains(player.PlayerId)) { Utils.SendMessage("\n", player.PlayerId, GetString("SpectateCommand.WasSpectatingLastRound")); @@ -358,14 +364,14 @@ internal static class ChatCommands Utils.SendMessage("\n", player.PlayerId, GetString("SpectateCommand.AlreadySpectating")); return; } - + Utils.SendMessage("\n", player.PlayerId, GetString("SpectateCommand.Success")); } - + private static void WhisperCommand(ChatController __instance, PlayerControl player, string text, string[] args) { if (!player.IsAlive()) return; - + if (Options.DisableWhisperCommand.GetBool()) { Utils.SendMessage("\n", player.PlayerId, GetString("WhisperDisabled")); @@ -380,10 +386,10 @@ internal static class ChatCommands if (args.Length < 3 || !byte.TryParse(args[1], out byte targetId)) return; if (!player.IsLocalPlayer()) ChatManager.SendPreviousMessagesToAll(); - + string msg = args[2..].Join(delimiter: " "); string title = string.Format(GetString("WhisperTitle"), player.PlayerId.ColoredPlayerName(), player.PlayerId); - + Utils.SendMessage(msg, targetId, title); ChatUpdatePatch.LastMessages.Add((msg, targetId, title, Utils.TimeStamp)); } @@ -478,7 +484,7 @@ internal static class ChatCommands Utils.SendMessage("\n", player.PlayerId, GetString("EveryoneReadyTitle")); else Utils.SendMessage(string.Join(", ", notReadyPlayers.Select(x => x.ColoredPlayerName())), player.PlayerId, string.Format(GetString("PlayersNotReadyTitle"), notReadyPlayers.Length)); - + if (Spectators.Count > 0) Utils.SendMessage(string.Join(", ", Spectators.Select(x => x.ColoredPlayerName())), player.PlayerId, string.Format(GetString("SpectatorsList"), Spectators.Count)); } } diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index ba22977ca..4bcc3f867 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -76,44 +76,44 @@ internal static class GameEndChecker { case CustomWinner.Crewmate: WinnerIds.UnionWith(Main.AllPlayerControls - .Where(pc => (pc.Is(CustomRoleTypes.Crewmate) || (pc.Is(CustomRoles.Haunter) && Haunter.CanWinWithCrew(pc))) && !pc.IsMadmate() && !pc.IsConverted() && !pc.Is(CustomRoles.EvilSpirit)) - .Select(pc => pc.PlayerId)); + .Where(pc => (pc.Is(CustomRoleTypes.Crewmate) || (pc.Is(CustomRoles.Haunter) && Haunter.CanWinWithCrew(pc))) && !pc.IsMadmate() && !pc.IsConverted() && !pc.Is(CustomRoles.EvilSpirit)) + .Select(pc => pc.PlayerId)); break; case CustomWinner.Impostor: WinnerIds.UnionWith(Main.AllPlayerControls - .Where(pc => ((pc.Is(CustomRoleTypes.Impostor) && (!pc.Is(CustomRoles.DeadlyQuota) || Main.PlayerStates.Count(x => x.Value.GetRealKiller() == pc.PlayerId) >= Options.DQNumOfKillsNeeded.GetInt())) || pc.IsMadmate()) && !pc.IsConverted() && !pc.Is(CustomRoles.EvilSpirit)) - .Select(pc => pc.PlayerId)); + .Where(pc => ((pc.Is(CustomRoleTypes.Impostor) && (!pc.Is(CustomRoles.DeadlyQuota) || Main.PlayerStates.Count(x => x.Value.GetRealKiller() == pc.PlayerId) >= Options.DQNumOfKillsNeeded.GetInt())) || pc.IsMadmate()) && !pc.IsConverted() && !pc.Is(CustomRoles.EvilSpirit)) + .Select(pc => pc.PlayerId)); break; case CustomWinner.Succubus: WinnerIds.UnionWith(Main.AllPlayerControls - .Where(pc => pc.Is(CustomRoles.Succubus) || pc.Is(CustomRoles.Charmed)) - .Select(pc => pc.PlayerId)); + .Where(pc => pc.Is(CustomRoles.Succubus) || pc.Is(CustomRoles.Charmed)) + .Select(pc => pc.PlayerId)); break; case CustomWinner.Necromancer: WinnerIds.UnionWith(Main.AllPlayerControls - .Where(pc => pc.Is(CustomRoles.Necromancer) || pc.Is(CustomRoles.Deathknight) || pc.Is(CustomRoles.Undead)) - .Select(pc => pc.PlayerId)); + .Where(pc => pc.Is(CustomRoles.Necromancer) || pc.Is(CustomRoles.Deathknight) || pc.Is(CustomRoles.Undead)) + .Select(pc => pc.PlayerId)); break; case CustomWinner.Virus: WinnerIds.UnionWith(Main.AllPlayerControls - .Where(pc => pc.Is(CustomRoles.Virus) || pc.Is(CustomRoles.Contagious)) - .Select(pc => pc.PlayerId)); + .Where(pc => pc.Is(CustomRoles.Virus) || pc.Is(CustomRoles.Contagious)) + .Select(pc => pc.PlayerId)); break; case CustomWinner.Jackal: WinnerIds.UnionWith(Main.AllPlayerControls - .Where(pc => pc.Is(CustomRoles.Jackal) || pc.Is(CustomRoles.Sidekick) || pc.Is(CustomRoles.Recruit)) - .Select(pc => pc.PlayerId)); + .Where(pc => pc.Is(CustomRoles.Jackal) || pc.Is(CustomRoles.Sidekick) || pc.Is(CustomRoles.Recruit)) + .Select(pc => pc.PlayerId)); break; case CustomWinner.Spiritcaller: WinnerIds.UnionWith(Main.AllPlayerControls - .Where(pc => pc.Is(CustomRoles.Spiritcaller) || pc.Is(CustomRoles.EvilSpirit)) - .Select(pc => pc.PlayerId)); + .Where(pc => pc.Is(CustomRoles.Spiritcaller) || pc.Is(CustomRoles.EvilSpirit)) + .Select(pc => pc.PlayerId)); WinnerRoles.Add(CustomRoles.Spiritcaller); break; @@ -229,6 +229,10 @@ internal static class GameEndChecker WinnerIds.Add(pc.PlayerId); AdditionalWinnerTeams.Add(AdditionalWinners.NoteKiller); break; + case CustomRoles.NecroGuesser when (Main.PlayerStates[pc.PlayerId].Role as NecroGuesser).GuessedPlayers >= NecroGuesser.NumGuessesToWin.GetInt(): + WinnerIds.Add(pc.PlayerId); + AdditionalWinnerTeams.Add(AdditionalWinners.NecroGuesser); + break; } } @@ -244,7 +248,7 @@ internal static class GameEndChecker case > 1 when WinnerIds.Any(x => GetPlayerById(x).Is(CustomRoles.Egoist)): WinnerIds.RemoveWhere(x => GetPlayerById(x).Is(CustomRoles.Egoist)); break; - // If there's only 1 impostor alive, and all alive impostors are Egoists, the Egoist wins alone + // If there's only 1 impostor alive, and all living impostors are Egoists, the Egoist wins alone case 1 when imps.All(x => x.Is(CustomRoles.Egoist)): PlayerControl pc = imps[0]; reason = GameOverReason.ImpostorByKill; diff --git a/Patches/GameStartManagerPatch.cs b/Patches/GameStartManagerPatch.cs index 30c99fe2c..584e3f121 100644 --- a/Patches/GameStartManagerPatch.cs +++ b/Patches/GameStartManagerPatch.cs @@ -288,10 +288,11 @@ public static class GameStartManagerPatch { if (AmongUsClient.Instance == null || AmongUsClient.Instance.IsGameStarted || GameStates.IsInGame || __instance == null || __instance.startState == GameStartManager.StartingStates.Starting) return; + var canStartGame = true; + var mismatchedClientName = string.Empty; + if (AmongUsClient.Instance.AmHost) { - var canStartGame = true; - foreach (ClientData client in AmongUsClient.Instance.allClients) { if (client.Character == null) continue; @@ -299,7 +300,11 @@ public static class GameStartManagerPatch var dummyComponent = client.Character.GetComponent(); if (dummyComponent != null && dummyComponent.enabled) continue; - if (!MatchVersions(client.Character.PlayerId, true)) canStartGame = false; + if (!MatchVersions(client.Character.PlayerId, true)) + { + canStartGame = false; + mismatchedClientName = client.Character.PlayerId.ColoredPlayerName(); + } } if (!canStartGame) __instance.StartButton.gameObject.SetActive(false); @@ -335,6 +340,12 @@ public static class GameStartManagerPatch if (Options.NoGameEnd.GetBool()) suffix = suffix.Insert(0, Utils.ColorString(Color.yellow, $"{GetString("NoGameEnd").ToUpper()}") + Utils.ColorString(Color.gray, " | ")); + if (!canStartGame) + { + suffix = suffix.Insert(0, Utils.ColorString(Color.red, string.Format(GetString("VersionMismatch"), mismatchedClientName)) + "\n"); + suffix += "\n"; + } + TextMeshPro tmp = GameStartManagerStartPatch.GameCountdown; if (tmp.text == string.Empty) diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index f515e865d..551f0cc47 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -29,7 +29,7 @@ internal static class CheckForEndVotingPatch if (Medic.PlayerIdList.Count > 0) Medic.OnCheckMark(); - //Meeting Skip with vote counting on keystroke (m + delete) + // Meeting Skip with vote counting bool shouldSkip = Input.GetKeyDown(KeyCode.F6); LogHandler voteLog = Logger.Handler("Vote"); @@ -124,7 +124,7 @@ internal static class CheckForEndVotingPatch !(Options.WhenSkipVoteIgnoreFirstMeeting.GetBool() && MeetingStates.FirstMeeting) && !(Options.WhenSkipVoteIgnoreNoDeadBody.GetBool() && !MeetingStates.IsExistDeadBody) && !(Options.WhenSkipVoteIgnoreEmergency.GetBool() && MeetingStates.IsEmergencyMeeting) - ) + ) { switch (Options.GetWhenSkipVote()) { @@ -160,25 +160,33 @@ internal static class CheckForEndVotingPatch } if (CheckRole(ps.TargetPlayerId, CustomRoles.Divinator) && Divinator.HideVote.GetBool()) continue; - if (CheckRole(ps.TargetPlayerId, CustomRoles.Eraser) && Eraser.HideVote.GetBool()) continue; - if (CheckRole(ps.TargetPlayerId, CustomRoles.NiceEraser) && NiceEraser.HideVote.GetBool()) continue; - if (CheckRole(ps.TargetPlayerId, CustomRoles.Scout) && Scout.HideVote.GetBool()) continue; - if (CheckRole(ps.TargetPlayerId, CustomRoles.Oracle) && Oracle.HideVote.GetBool()) continue; - if (ps.TargetPlayerId == ps.VotedFor && Options.MadmateSpawnMode.GetInt() == 2 && CustomRoles.Madmate.IsEnable() && MeetingStates.FirstMeeting) continue; - AddVote(); + bool canVote = !(CheckRole(ps.TargetPlayerId, CustomRoles.Glitch) && !Glitch.CanVote.GetBool()); + if (CheckRole(ps.TargetPlayerId, CustomRoles.Shifter) && !Shifter.CanVote.GetBool()) canVote = false; + if (ps.VotedFor.GetPlayer() != null && CheckRole(ps.VotedFor, CustomRoles.Zombie)) canVote = false; - if (Main.PlayerStates[ps.TargetPlayerId].Role is Adventurer { IsEnable: true } av && av.ActiveWeapons.Contains(Adventurer.Weapon.Proxy)) AddVote(); + switch (Main.PlayerStates[ps.TargetPlayerId].Role) + { + case Adventurer { IsEnable: true } av when av.ActiveWeapons.Contains(Adventurer.Weapon.Proxy): + AddVote(); + break; + case Amogus { IsEnable: true, ExtraVotes: > 0 } amogus: + Loop.Times(amogus.ExtraVotes, _ => AddVote()); + break; + case Dad { IsEnable: true } dad when dad.UsingAbilities.Contains(Dad.Ability.GoForMilk): + canVote = false; + break; + } + + if (canVote) AddVote(); if (CheckRole(ps.TargetPlayerId, CustomRoles.Mayor) && !Mayor.MayorHideVote.GetBool()) Loop.Times(Mayor.MayorAdditionalVote.GetInt(), _ => AddVote()); - if (CheckRole(ps.TargetPlayerId, CustomRoles.Vindicator) && !Options.VindicatorHideVote.GetBool()) Loop.Times(Options.VindicatorAdditionalVote.GetInt(), _ => AddVote()); - if (CheckRole(ps.TargetPlayerId, CustomRoles.Knighted)) AddVote(); if (CheckRole(ps.TargetPlayerId, CustomRoles.DualPersonality) && Options.DualVotes.GetBool()) @@ -363,9 +371,7 @@ internal static class CheckForEndVotingPatch } if (Options.ConfirmEgoistOnEject.GetBool() && player.Is(CustomRoles.Egoist)) coloredRole = Utils.ColorString(Utils.GetRoleColor(CustomRoles.Egoist), GetRoleString("Temp.Blank") + coloredRole.RemoveHtmlTags()); - if (Options.ConfirmLoversOnEject.GetBool() && Main.LoversPlayers.Exists(x => x.PlayerId == player.PlayerId)) coloredRole = Utils.ColorString(Utils.GetRoleColor(CustomRoles.Lovers), GetRoleString("Temp.Blank") + coloredRole.RemoveHtmlTags()); - if (Options.RascalAppearAsMadmate.GetBool() && player.Is(CustomRoles.Rascal)) coloredRole = Utils.ColorString(Utils.GetRoleColor(CustomRoles.Madmate), GetRoleString("Mad-") + coloredRole.RemoveHtmlTags()); var name = string.Empty; @@ -574,16 +580,10 @@ internal static class CheckForEndVotingPatch if (Main.PlayerStates.TryGetValue(id, out PlayerState state) && state.SubRoles.Contains(CustomRoles.Avanger)) RevengeOnExile(playerId /*, deathReason*/); } - catch (Exception e) - { - Utils.ThrowException(e); - } + catch (Exception e) { Utils.ThrowException(e); } } } - catch (Exception e) - { - Utils.ThrowException(e); - } + catch (Exception e) { Utils.ThrowException(e); } } private static void RevengeOnExile(byte playerId /*, PlayerState.DeathReason deathReason*/) @@ -659,6 +659,9 @@ internal static class ExtendedMeetingHud case Dad { IsEnable: true } dad when dad.UsingAbilities.Contains(Dad.Ability.GoForMilk): VoteNum = 0; break; + case Amogus { IsEnable: true, ExtraVotes: > 0 } amogus: + VoteNum += amogus.ExtraVotes; + break; } if (ps.TargetPlayerId == ps.VotedFor && Options.MadmateSpawnMode.GetInt() == 2 && CustomRoles.Madmate.IsEnable() && MeetingStates.FirstMeeting) VoteNum = 0; @@ -783,13 +786,9 @@ internal static class MeetingHudStartPatch } if (Mortician.MsgToSend.TryGetValue(pc.PlayerId, out string value2)) AddMsg(value2, pc.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Mortician), GetString("MorticianCheckTitle"))); - if (Mediumshiper.ContactPlayer.ContainsValue(pc.PlayerId)) AddMsg(string.Format(GetString("MediumshipNotifySelf"), Main.AllPlayerNames[Mediumshiper.ContactPlayer.FirstOrDefault(x => x.Value == pc.PlayerId).Key], pc.GetAbilityUseLimit()), pc.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Mediumshiper), GetString("MediumshipTitle"))); - if (Mediumshiper.ContactPlayer.ContainsKey(pc.PlayerId) && (!Mediumshiper.OnlyReceiveMsgFromCrew.GetBool() || pc.IsCrewmate())) AddMsg(string.Format(GetString("MediumshipNotifyTarget"), Main.AllPlayerNames[Mediumshiper.ContactPlayer[pc.PlayerId]]), pc.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Mediumshiper), GetString("MediumshipTitle"))); - if (Virus.VirusNotify.TryGetValue(pc.PlayerId, out string value3)) AddMsg(value3, pc.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Virus), GetString("VirusNoticeTitle"))); - if (Enigma.MsgToSend.TryGetValue(pc.PlayerId, out string value4)) AddMsg(value4, pc.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Enigma), Enigma.MsgToSendTitle[pc.PlayerId])); if (QuizMaster.On && QuizMaster.MessagesToSend.TryGetValue(pc.PlayerId, out string value5)) @@ -1228,10 +1227,7 @@ internal static class MeetingHudCastVotePatch PlayerVoteArea pva_src = info.SourcePVA; PlayerControl pc_src = info.SourcePC; - try - { - pva_src.UnsetVote(); - } + try { pva_src.UnsetVote(); } catch { } __instance.RpcClearVote(pc_src.GetClientId()); diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index db4a05f04..372685551 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -111,7 +111,7 @@ internal static class EndGamePatch Main.RealOptionsData.Restore(GameOptionsManager.Instance.CurrentGameOptions); GameOptionsSender.AllSenders.Clear(); GameOptionsSender.AllSenders.Add(new NormalGameOptionsSender()); - + ChatCommands.Spectators.Clear(); switch (Options.CurrentGameMode) @@ -496,6 +496,14 @@ internal static class SetEverythingUpPatch sb.Append(""); + var winCounts = Utils.GetWinCountsString(); + + if (winCounts != string.Empty) + { + sb.Append("\n\n"); + sb.Append(winCounts); + } + var RoleSummary = RoleSummaryObject.GetComponent(); RoleSummary.alignment = TextAlignmentOptions.TopLeft; RoleSummary.color = Color.white; diff --git a/Patches/PhantomRolePatch.cs b/Patches/PhantomRolePatch.cs index dc5c72e8a..e049b50fe 100644 --- a/Patches/PhantomRolePatch.cs +++ b/Patches/PhantomRolePatch.cs @@ -1,6 +1,8 @@ using System; using System.Collections; using AmongUs.GameOptions; +using EHR.Crewmate; +using EHR.Impostor; using HarmonyLib; using Hazel; using Il2CppInterop.Runtime.InteropTypes.Arrays; @@ -52,12 +54,37 @@ public static class PhantomRolePatch // Called when Phantom press vanish button when visible [HarmonyPatch(nameof(PlayerControl.CheckVanish))] [HarmonyPrefix] - private static void CheckVanish_Prefix(PlayerControl __instance) + private static bool CheckVanish_Prefix(PlayerControl __instance) { - if (!AmongUsClient.Instance.AmHost) return; + if (!AmongUsClient.Instance.AmHost) return true; PlayerControl phantom = __instance; - Logger.Info($"Player: {phantom.GetRealName()}", "CheckVanish"); + Logger.Info($"Player: {phantom.GetNameWithRole()}", "CheckVanish"); + + if (!Rhapsode.CheckAbilityUse(phantom) || Stasis.IsTimeFrozen || !Main.PlayerStates[__instance.PlayerId].Role.OnVanish(__instance)) + { + if (phantom.AmOwner) + { + DestroyableSingleton.Instance.AbilityButton.SetFromSettings(phantom.Data.Role.Ability); + phantom.Data.Role.SetCooldown(); + return false; + } + + CustomRpcSender sender = CustomRpcSender.Create($"Cancel vanish for {phantom.GetRealName()}"); + sender.StartMessage(phantom.GetClientId()); + + sender.StartRpc(phantom.NetId, (byte)RpcCalls.SetRole) + .Write((ushort)RoleTypes.Phantom) + .Write(true) + .EndRpc(); + + sender.EndMessage(); + sender.SendMessage(); + + phantom.RpcResetAbilityCooldown(); + + LateTask.New(() => phantom.SetKillCooldown(Math.Max(Main.KillTimers[phantom.PlayerId], 0.001f)), 0.2f); + } foreach (PlayerControl target in Main.AllPlayerControls) { @@ -67,22 +94,24 @@ public static class PhantomRolePatch phantom.RpcCheckVanishDesync(target); LateTask.New(() => - { - if (GameStates.IsMeeting || phantom == null) return; + { + if (GameStates.IsMeeting || phantom == null) return; - string petId = phantom.Data.DefaultOutfit.PetId; + string petId = phantom.Data.DefaultOutfit.PetId; - if (petId != "") - { - PetsList[phantom.PlayerId] = petId; - phantom.RpcSetPetDesync("", target); - } + if (petId != "") + { + PetsList[phantom.PlayerId] = petId; + phantom.RpcSetPetDesync("", target); + } - phantom.RpcExileDesync(target); - }, 1.2f, $"Set Phantom invisible {target.PlayerId}"); + phantom.RpcExileDesync(target); + }, 1.2f, $"Set Phantom invisible {target.PlayerId}"); } InvisibilityList.Add(phantom); + + return true; } [HarmonyPatch(nameof(PlayerControl.CheckAppear))] @@ -105,19 +134,19 @@ public static class PhantomRolePatch phantom.RpcSetRoleDesync(RoleTypes.Phantom, clientId); LateTask.New(() => - { - if (target != null) phantom.RpcCheckAppearDesync(shouldAnimate, target); - }, 0.5f, $"Check Appear when vanish is over {target.PlayerId}"); + { + if (target != null) phantom.RpcCheckAppearDesync(shouldAnimate, target); + }, 0.5f, $"Check Appear when vanish is over {target.PlayerId}"); LateTask.New(() => - { - if (GameStates.IsMeeting || phantom == null) return; + { + if (GameStates.IsMeeting || phantom == null) return; - InvisibilityList.Remove(phantom); - phantom.RpcSetRoleDesync(RoleTypes.Scientist, clientId); + InvisibilityList.Remove(phantom); + phantom.RpcSetRoleDesync(RoleTypes.Scientist, clientId); - if (PetsList.TryGetValue(phantom.PlayerId, out string petId)) phantom.RpcSetPetDesync(petId, target); - }, 1.8f, $"Set Scientist when vanish is over {target.PlayerId}"); + if (PetsList.TryGetValue(phantom.PlayerId, out string petId)) phantom.RpcSetPetDesync(petId, target); + }, 1.8f, $"Set Scientist when vanish is over {target.PlayerId}"); } } @@ -147,10 +176,7 @@ public static class PhantomRolePatch Main.Instance.StartCoroutine(CoRevertInvisible(phantom, seer, force)); } } - catch (Exception e) - { - Utils.ThrowException(e); - } + catch (Exception e) { Utils.ThrowException(e); } } private static bool InValid(PlayerControl phantom, PlayerControl seer) diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index ad1c3b889..3e2abb908 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -777,7 +777,7 @@ internal static class ShapeshiftPatch if (AmongUsClient.Instance.AmHost && shapeshifting) { - if (!Rhapsode.CheckAbilityUse(shapeshifter)) return false; + if (!Rhapsode.CheckAbilityUse(shapeshifter) || Stasis.IsTimeFrozen) return false; if (shapeshifter.Is(CustomRoles.Trainee) && MeetingStates.FirstMeeting) { @@ -1988,7 +1988,7 @@ internal static class CoEnterVentPatch return true; } - if (!Rhapsode.CheckAbilityUse(__instance.myPlayer)) + if (!Rhapsode.CheckAbilityUse(__instance.myPlayer) || Stasis.IsTimeFrozen) { LateTask.New(() => __instance.RpcBootFromVent(id), 0.5f, log: false); return true; @@ -2290,19 +2290,6 @@ internal static class CmdCheckAppearPatch } } -[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckVanish))] -internal static class CheckVanishPatch -{ - public static bool Prefix(PlayerControl __instance) - { - Logger.Info($" {__instance.GetNameWithRole()}", "CheckVanish"); - bool allow = Main.PlayerStates[__instance.PlayerId].Role.OnVanish(__instance); - if (!allow) LateTask.New(__instance.RpcResetAbilityCooldown, 0.2f, log: false); - - return true; - } -} - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.AssertWithTimeout))] internal static class AssertWithTimeoutPatch { diff --git a/Patches/SabotageSystemPatch.cs b/Patches/SabotageSystemPatch.cs index 8864adef8..dada2014c 100644 --- a/Patches/SabotageSystemPatch.cs +++ b/Patches/SabotageSystemPatch.cs @@ -1,4 +1,5 @@ using EHR.Crewmate; +using EHR.Impostor; using EHR.Modules; using EHR.Neutral; using HarmonyLib; @@ -365,8 +366,9 @@ public static class SabotageSystemTypeRepairDamagePatch return false; } - if (player.Is(Team.Impostor) && !player.IsAlive() && Options.DeadImpCantSabotage.GetBool()) return false; + if (Rhapsode.CheckAbilityUse(player) || Stasis.IsTimeFrozen) return false; + if (player.Is(Team.Impostor) && !player.IsAlive() && Options.DeadImpCantSabotage.GetBool()) return false; if (!player.Is(Team.Impostor) && !player.IsAlive()) return false; bool allow = player.GetCustomRole() switch diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index f99e330ac..c46f00278 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -2422,6 +2422,7 @@ "WhisperTitle": "{0} <#ffffff>(ID <#00ffa5>{1}) whispers to you:", "DisableWhisperCommand": "Disable /whisper command", "DisableSpectateCommand": "Disable /spectate command", + "SpectateDisabled": "<#ff0000>X The /spectate command is disabled in this lobby.", "WhisperDisabled": "<#ff0000>X The /whisper command is disabled in this lobby.", "ThankYouForUsingEHR": "Thank you for using EHR!", "GMRoles": "【 ★ Roles ★ 】", @@ -2676,9 +2677,10 @@ "Outside": "Outside", "UnsupportedVersion": "Unsupported Among Us version.\\nPlease update Among Us.", "Error.MeetingException": "Error: {0}\\r\\nPlease use SHIFT+M+ENTER to end the meeting", - "Error.InvalidRoleAssignment": "Error: Invalid role found for a player during role assignment({0})", - "Error.InvalidColor": "Error: Only default colors are available", - "Error.InvalidColorPreventStart": "Other players are not allowed to use other colors, otherwise it will result in a serious error", + "Error.InvalidRoleAssignment": "Error: Invalid role found for a player during role assignment ({0})", + "Error.InvalidColor": "Error: Only default colors are available - this means someone had fortegreen color in the lobby, which causes disconnects, so it's best to kick them", + "Error.InvalidColorPreventStart": "Other players are not allowed to use other colors (e.g. fortegreen), otherwise it will result in a serious error", + "VersionMismatch": "{0} has a different version of the mod installed", "ErrorLevel1": "Bugs may occur.", "ErrorLevel2": "This can cause serious issues with the game.", "ErrorLevel3": "This version shouldn't have been released.", @@ -2808,6 +2810,7 @@ "AntiAdminerVI": "⚠ Vitals Monitor ⚠", "AntiAdminerDL": "⚠ DoorLog ⚠", "AntiAdminerCA": "⚠ Security Cameras ⚠", + "WinsCountTitle": "Number of wins in this game mode", "Roles": "Roles", "Settings": "Settings", "Addons": "Add-Ons", @@ -2956,9 +2959,9 @@ "Disabled": "Disabled", "FailToTrack": "Failed To Track", "CanGuessTarget": "Can Guess their Target", - "UsePhantomBasis": "Use Phantom Basis for some roles (DEPRECATED, use Unshift Trigger ^^)", + "UsePhantomBasis": "Use Vanish Button (Phantom basis) as Ability Trigger", "UsePhantomBasisForNKs": "Even for <#ffab1b>Neutral <#ff1919>Killing (Desynced) roles <#ffff00>(EXPERIMENTAL)", - "UseUnshiftTrigger": "Use Unshift Button as Ability Trigger", + "UseUnshiftTrigger": "Use Unshift Button (Shapeshifter basis) as Ability Trigger", "UseUnshiftTriggerForNKs": "Even for <#ffab1b>Neutral <#ff1919>Killing (Desynced) roles <#ffff00>(EXPERIMENTAL)", "Mad-": "Mad ", "Charmed-": "Charmed ", @@ -3837,6 +3840,25 @@ "HHNumOfTargets": "Number of Targets", "HHMaxKCD": "Maximum Kill Cooldown", "HHMinKCD": "Minimum Kill Cooldown", + "Stasis": "Stasis", + "StasisInfo": "Freeze Time", + "StasisInfoLong": "(Impostors):\\nAs the Stasis, you can shapeshift/pet/vanish to 'freeze time'. That means, all players will be frozen and blind, and no one will be able to do anything. Depending on the settings, the blinding and freezing may exclude other impostor-teamed players.", + "Stasis.AffectsOtherImpostors": "Time Freezing Affects Other <#ff1919>Impostors", + "Stasis.TimeFrozenNotify": "Time Frozen", + "Amogus": "Amogus", + "AmogusInfo": "You're the sussy baka", + "AmogusInfoLong": "(Neutrals):\\nAs the Amogus, you can thank Rabek for the role idea. Lol.\\nAnyway, Amogus is a neutral killer that can use shift/vanish/pet button to switch between regular and amogus form. You can vent and you have impostor vision. You can only kill in amogus form. You can stay in amogus form for a limited amount of time. After killing someone, your amogus form upgrades. These are the levels:\\nAmogus -> Sugoma -> Sumogus -> Suspicious Sus -> Ultimate Sus\\nWhen you're in amogus form, you're anonymous and look different depending on your level. Levels also give extra abilities that work only in amogus form.\\n\\nAmogus: Starting level, regular white amogus without extra special abilities.\\nSugoma: Upside down amogus. Is faster, but your controls are inverted.\\nSumogus: Wide amogus with two visors. No one can interact with you and you have sugoma ability, but your controls aren't inverted anymore.\\nSuspicious Sus: Amogus that is robot. You have all abilities of Sumogus and in addition you can see an arrow to every player.\\nUltimate Sus: Red amogus. Has all abilities of lower levels. Your kill cooldown is reduced by 50%. If you kill someone, you get additional vote(s).", + "Amogus.StartingLevel": "Starting Level", + "Amogus.SugomaSpeed": "Speed at Sugoma level", + "Amogus.SuspiciousSusArrowsToBodies": "At Suspicious Sus level, when someone dies, the arrow points at the dead body instead of disappearing", + "Amogus.UltimateSusVotesPerKill": "Number of Extra Votes gained for every kill, when the level is Ultimate Sus", + "Amogus.Suffix": "<#00ffa5>Current level: {0}", + "Amogus.Levels.Amogus": "Amogus", + "Amogus.Levels.Sugoma": "Sugoma", + "Amogus.Levels.Sumogus": "Sumogus", + "Amogus.Levels.SuspiciousSus": "Suspicious Sus", + "Amogus.Levels.UltimateSus": "Ultimate Sus", + "ExtraVotesPT": "<#00a5ff>(<#00ffff>+<#00ffa5>{0} votes)", "Rabbit": "Rabbit", "RabbitInfo": "Get Clues Toward Impostor Locations", "RabbitInfoLong": "(Crewmates):\\nAs the Rabbit, each time you complete a task, you see an arrow pointing towards a random impostor for 5 seconds.\\nNote: This ability may only activate after you've completed a set number of tasks.", @@ -4111,7 +4133,7 @@ "DeliverCooldown": "Deliver Cooldown", "NecroGuesser": "Necro-Guesser", "NecroGuesserInfo": "Guess Dead Players' Roles", - "NecroGuesserInfoLong": "(Neutrals):\\nAs the Necro-Guesser, your job is to guess the roles of dead players. Use /bt {id} {role} to guess. {id} is the player's ID, biew them by using /id, {role} is the player's role. You need to guess a certain amount of dead players' roles to win with the winning team at the end.", + "NecroGuesserInfoLong": "(Neutrals):\\nAs the Necro-Guesser, your job is to guess the roles of dead players. Use /bt {id} {role} to guess. {id} is the player's ID, view them by using /id, {role} is the player's role. You need to guess a certain amount of dead players' roles to win with the winning team at the end.", "NecroGuesser.NumGuessesToWin": "Number of Guesses Required to Win", "NecroGuesser.GuessCorrect": "Your guess was <#00ff00>correct!", "NecroGuesser.GuessIncorrect": "Your guess was <#ff0000>incorrect!", diff --git a/Roles/Crewmate/ParityCop.cs b/Roles/Crewmate/ParityCop.cs index bc29acb34..28fbc2eeb 100644 --- a/Roles/Crewmate/ParityCop.cs +++ b/Roles/Crewmate/ParityCop.cs @@ -315,26 +315,29 @@ public class ParityCop : RoleBase switch (firstRoleClass) { - case Lawyer when Lawyer.Target[first.PlayerId] == second.PlayerId: return true; - case Totocalcio tc when tc.BetPlayer == second.PlayerId: return true; - case Romantic when Romantic.HasPickedPartner && Romantic.PartnerId == second.PlayerId: return true; - case Necromancer when secondRoleClass is Deathknight: return true; + case Lawyer when Lawyer.Target[first.PlayerId] == second.PlayerId: + case Totocalcio tc when tc.BetPlayer == second.PlayerId: + case Romantic when Romantic.HasPickedPartner && Romantic.PartnerId == second.PlayerId: + case Necromancer when secondRoleClass is Deathknight: + return true; } switch (secondRoleClass) { - case Lawyer when Lawyer.Target[second.PlayerId] == first.PlayerId: return true; - case Totocalcio tc when tc.BetPlayer == first.PlayerId: return true; - case Romantic when Romantic.HasPickedPartner && Romantic.PartnerId == first.PlayerId: return true; - case Necromancer when firstRoleClass is Deathknight: return true; + case Lawyer when Lawyer.Target[second.PlayerId] == first.PlayerId: + case Totocalcio tc when tc.BetPlayer == first.PlayerId: + case Romantic when Romantic.HasPickedPartner && Romantic.PartnerId == first.PlayerId: + case Necromancer when firstRoleClass is Deathknight: + return true; } if (CustomTeamManager.AreInSameCustomTeam(first.PlayerId, second.PlayerId)) return true; - if (firstSubRoles.Contains(CustomRoles.Bloodlust) || secondSubRoles.Contains(CustomRoles.Bloodlust)) return false; - if (firstRole.IsNeutral() && secondRole.IsNeutral()) return false; + if (firstSubRoles.Contains(CustomRoles.Rascal) && secondTeam != Team.Impostor) return false; + if (secondSubRoles.Contains(CustomRoles.Rascal) && firstTeam != Team.Impostor) return false; + return firstTeam == secondTeam || firstRole == secondRole; } diff --git a/Roles/Impostor/Stasis.cs b/Roles/Impostor/Stasis.cs new file mode 100644 index 000000000..1117c7aa8 --- /dev/null +++ b/Roles/Impostor/Stasis.cs @@ -0,0 +1,135 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; +using UnityEngine; + +namespace EHR.Impostor; + +public class Stasis : RoleBase +{ + public static bool On; + private static List Instances = []; + + public static OptionItem AbilityCooldown; + public static OptionItem AbilityDuration; + private static OptionItem AffectsOtherImpostors; + private static OptionItem CanVent; + private static OptionItem KillCooldown; + + private bool UsingAbility; + + public override bool IsEnable => On; + + public static bool IsTimeFrozen => Instances.Any(x => x.UsingAbility); + + public override void SetupCustomOption() + { + StartSetup(649025, true) + .AutoSetupOption(ref AbilityCooldown, 30, new IntegerValueRule(1, 60, 1), OptionFormat.Seconds) + .AutoSetupOption(ref AbilityDuration, 10, new IntegerValueRule(1, 60, 1), OptionFormat.Seconds) + .AutoSetupOption(ref AffectsOtherImpostors, true) + .AutoSetupOption(ref CanVent, false) + .AutoSetupOption(ref KillCooldown, 30f, new FloatValueRule(0f, 180f, 1f), OptionFormat.Seconds); + } + + public override void Init() + { + On = false; + Instances = []; + } + + public override void Add(byte playerId) + { + On = true; + Instances.Add(this); + UsingAbility = false; + } + + public override bool CanUseImpostorVentButton(PlayerControl pc) + { + return CanVent.GetBool(); + } + + public override void SetKillCooldown(byte id) + { + Main.AllPlayerKillCooldown[id] = KillCooldown.GetFloat(); + } + + public override void ApplyGameOptions(IGameOptions opt, byte playerId) + { + if (Options.UsePets.GetBool()) return; + + AURoleOptions.ShapeshifterCooldown = AbilityCooldown.GetInt(); + AURoleOptions.ShapeshifterDuration = 1f; + } + + public override bool OnShapeshift(PlayerControl shapeshifter, PlayerControl target, bool shapeshifting) + { + if (!shapeshifting && !Options.UseUnshiftTrigger.GetBool()) return true; + + OnPet(shapeshifter); + return false; + } + + public override bool OnVanish(PlayerControl pc) + { + OnPet(pc); + return false; + } + + public override void OnPet(PlayerControl pc) + { + UsingAbility = true; + + ReportDeadBodyPatch.CanReport.SetAllValues(false); + Main.AllPlayerSpeed.SetAllValues(Main.MinSpeed); + Main.PlayerStates.Values.DoIf(x => !x.IsDead, x => x.IsBlackOut = true); + + int time = AbilityDuration.GetInt(); + + foreach (PlayerControl player in Main.AllPlayerControls) + { + if (!player.IsAlive() || player.PlayerId == pc.PlayerId || (player.Is(Team.Impostor) && !AffectsOtherImpostors.GetBool())) + { + ReportDeadBodyPatch.CanReport[player.PlayerId] = true; + Main.AllPlayerSpeed[player.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); + Main.PlayerStates[player.PlayerId].IsBlackOut = false; + } + else + { + if (Main.KillTimers[player.PlayerId] < time) player.SetKillCooldown(time); + player.RpcResetAbilityCooldown(); + + if (!player.HasAbilityCD() && player.GetCustomRole().PetActivatedAbility()) + player.AddAbilityCD(time); + } + } + + Utils.SyncAllSettings(); + Main.Instance.StartCoroutine(Countdown()); + return; + + IEnumerator Countdown() + { + var imps = AffectsOtherImpostors.GetBool() ? [pc] : Main.AllAlivePlayerControls.Where(x => x.Is(Team.Impostor)).ToArray(); + + for (int i = 0; i < time; i++) + { + // ReSharper disable once AccessToModifiedClosure + imps.Do(x => x.Notify($"<#00ffa5>{Translator.GetString("Stasis.TimeFrozenNotify")} <#888888>- {time - i}", overrideAll: true)); + yield return new WaitForSeconds(1f); + } + + UsingAbility = false; + imps.Do(x => x.Notify(Translator.GetString("Stasis.TimeFreezeEndNotify"), overrideAll: true)); + + ReportDeadBodyPatch.CanReport.SetAllValues(true); + Main.AllPlayerSpeed.SetAllValues(Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod)); + Main.PlayerStates.Values.Do(x => x.IsBlackOut = false); + Utils.SyncAllSettings(); + + pc.RpcResetAbilityCooldown(); + } + } +} \ No newline at end of file diff --git a/Roles/Neutral/Amogus.cs b/Roles/Neutral/Amogus.cs index 2773c06d5..800cd9e9c 100644 --- a/Roles/Neutral/Amogus.cs +++ b/Roles/Neutral/Amogus.cs @@ -10,34 +10,25 @@ namespace EHR.Neutral; public class Amogus : RoleBase { public static bool On; - public static List Instances = []; + private static List Instances = []; private static OptionItem StartingLevel; private static OptionItem SugomaSpeed; private static OptionItem SuspiciousSusArrowsToBodies; private static OptionItem UltimateSusVotesPerKill; public static OptionItem AbilityCooldown; - private static OptionItem AbilityDuration; + public static OptionItem AbilityDuration; private static OptionItem KillCooldown; private static OptionItem CanVent; private static OptionItem ImpostorVision; - - public override bool IsEnable => On; - - enum Levels - { - Amogus, - Sugoma, - Sumogus, - SuspiciousSus, - UltimateSus - } + private long AmogusFormEndTS; private byte AmogusID; - private long AmogusFormEndTS; private Levels CurrentLevel; public int ExtraVotes; + public override bool IsEnable => On; + public override void SetupCustomOption() { StartSetup(649075) @@ -107,9 +98,7 @@ public class Amogus : RoleBase public override void OnPet(PlayerControl pc) { if (CurrentLevel >= Levels.SuspiciousSus) - { Main.AllAlivePlayerControls.Do(x => TargetArrow.Add(AmogusID, x.PlayerId)); - } AmogusFormEndTS = Utils.TimeStamp + AbilityDuration.GetInt(); Utils.SendRPC(CustomRPC.SyncRoleData, AmogusID, 1, AmogusFormEndTS); @@ -135,6 +124,7 @@ public class Amogus : RoleBase if (!GameStates.IsInTask || ExileController.Instance || !pc.IsAlive()) return; long now = Utils.TimeStamp; + if (AmogusFormEndTS != 0 && AmogusFormEndTS <= now) { FormExpired(); @@ -146,7 +136,7 @@ public class Amogus : RoleBase { TargetArrow.RemoveAllTarget(AmogusID); LocateArrow.RemoveAllTarget(AmogusID); - + AmogusFormEndTS = 0; Utils.SendRPC(CustomRPC.SyncRoleData, AmogusID, 1, AmogusFormEndTS); } @@ -157,9 +147,8 @@ public class Amogus : RoleBase { CurrentLevel++; Utils.SendRPC(CustomRPC.SyncRoleData, AmogusID, 2, (int)CurrentLevel); - Utils.NotifyRoles(SpecifySeer: killer, SpecifyTarget: killer); } - + switch (CurrentLevel) { case Levels.SuspiciousSus: @@ -167,6 +156,7 @@ public class Amogus : RoleBase break; case Levels.UltimateSus: ExtraVotes += UltimateSusVotesPerKill.GetInt(); + Utils.SendRPC(CustomRPC.SyncRoleData, AmogusID, 3, ExtraVotes); break; } } @@ -200,6 +190,9 @@ public class Amogus : RoleBase case 2: CurrentLevel = (Levels)reader.ReadPackedInt32(); break; + case 3: + ExtraVotes = reader.ReadPackedInt32(); + break; } } @@ -209,7 +202,23 @@ public class Amogus : RoleBase var sb = new StringBuilder(); if (AmogusFormEndTS != 0) sb.Append($"\u25a9 ({Utils.TimeStamp - AmogusFormEndTS}s)\n"); + if (!hud) sb.Append(""); sb.Append(string.Format(Translator.GetString("Amogus.Suffix"), Translator.GetString($"Amogus.Levels.{CurrentLevel}"))); + if (!hud) sb.Append(""); return sb.ToString(); } + + public override string GetProgressText(byte playerId, bool comms) + { + return $"{base.GetProgressText(playerId, comms)} {string.Format(Translator.GetString("ExtraVotesPT"), ExtraVotes)}"; + } + + enum Levels + { + Amogus, + Sugoma, + Sumogus, + SuspiciousSus, + UltimateSus + } } \ No newline at end of file diff --git a/Roles/Neutral/NecroGuesser.cs b/Roles/Neutral/NecroGuesser.cs index 16fa7bd8b..74d19d379 100644 --- a/Roles/Neutral/NecroGuesser.cs +++ b/Roles/Neutral/NecroGuesser.cs @@ -4,12 +4,12 @@ public class NecroGuesser : RoleBase { public static bool On; - public override bool IsEnable => On; - - private static OptionItem NumGuessesToWin; + public static OptionItem NumGuessesToWin; public int GuessedPlayers; + public override bool IsEnable => On; + public override void SetupCustomOption() { StartSetup(647125) diff --git a/Roles/Neutral/NoteKiller.cs b/Roles/Neutral/NoteKiller.cs index 92b0ebad0..c07789352 100644 --- a/Roles/Neutral/NoteKiller.cs +++ b/Roles/Neutral/NoteKiller.cs @@ -49,7 +49,7 @@ public class NoteKiller : RoleBase .AutoSetupOption(ref NumLettersRevealed, 2, new IntegerValueRule(1, Names.Max(x => x.Length), 1)) .AutoSetupOption(ref ClueShowDuration, 5, new IntegerValueRule(0, 30, 1), OptionFormat.Seconds) .AutoSetupOption(ref WinCondition, 0, WinConditions) - .AutoSetupOption(ref NumPlayersToKill, 2, new IntegerValueRule(0, 14, 1)) + .AutoSetupOption(ref NumPlayersToKill, 2, new IntegerValueRule(0, 14, 1), overrideParent: WinCondition) .AutoSetupOption(ref CanVent, true) .AutoSetupOption(ref ImpostorVision, true); } diff --git a/Roles/Neutral/Tank.cs b/Roles/Neutral/Tank.cs index 64c999fd0..9cc465c2c 100644 --- a/Roles/Neutral/Tank.cs +++ b/Roles/Neutral/Tank.cs @@ -23,7 +23,7 @@ public class Tank : RoleBase public override void SetupCustomOption() { StartSetup(646950) - .AutoSetupOption(ref Speed, 0.7f, new FloatValueRule(0.1f, 3f, 0.1f), OptionFormat.Multiplier) + .AutoSetupOption(ref Speed, 0.9f, new FloatValueRule(0.1f, 3f, 0.1f), OptionFormat.Multiplier) .AutoSetupOption(ref CanBeGuessed, false) .AutoSetupOption(ref CanBeKilled, true) .AutoSetupOption(ref VentCooldown, 15f, new FloatValueRule(0f, 60f, 0.5f), OptionFormat.Seconds); @@ -77,4 +77,9 @@ public class Tank : RoleBase string randomVentName = ShipStatus.Instance?.AllVents?.FirstOrDefault(x => x.Id == AllVents.Except(EnteredVents).FirstOrDefault())?.name ?? string.Empty; return randomVentName == string.Empty ? string.Empty : string.Format(Translator.GetString("Tank.Suffix"), randomVentName); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || (!EnteredVents.Contains(ventId) && pc.GetClosestVent()?.Id == ventId); + } } \ No newline at end of file diff --git a/Roles/Neutral/Vulture.cs b/Roles/Neutral/Vulture.cs index 380f5951e..aecd31a8b 100644 --- a/Roles/Neutral/Vulture.cs +++ b/Roles/Neutral/Vulture.cs @@ -67,7 +67,7 @@ public class Vulture : RoleBase .SetParent(CustomRoleSpawnChances[CustomRoles.Vulture]); ChangeRole = new StringOptionItem(Id + 17, "VultureChangeRole", ChangeRoles.Select(x => x.ToColoredString()).ToArray(), 0, TabGroup.NeutralRoles, noTranslation: true) - .SetParent(CustomRoleSpawnChances[CustomRoles.Vulture]); + .SetParent(ChangeRoleWhenCantWin); } public override void Init() diff --git a/main.cs b/main.cs index 601698c34..c6e84c3b4 100644 --- a/main.cs +++ b/main.cs @@ -108,6 +108,7 @@ public class Main : BasePlugin public static string FirstDied = string.Empty; public static string ShieldPlayer = string.Empty; + public static Dictionary> NumWinsPerGM = []; public static HashSet DiedThisRound = []; public static List LoversPlayers = []; public static bool IsLoversDead = true; @@ -740,9 +741,9 @@ public class Main : BasePlugin try { AllRoleClasses.AddRange(Assembly.GetAssembly(typeof(RoleBase))! - .GetTypes() - .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(RoleBase))) - .Select(t => (RoleBase)Activator.CreateInstance(t, null))); + .GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(RoleBase))) + .Select(t => (RoleBase)Activator.CreateInstance(t, null))); AllRoleClasses.Sort(); } @@ -957,4 +958,4 @@ public enum TieMode Random } -public class Coroutines : MonoBehaviour { } +public class Coroutines : MonoBehaviour { } \ No newline at end of file