diff --git a/Gamemodes/CaptureTheFlag.cs b/Gamemodes/CaptureTheFlag.cs index 93b5ae128..439ca3382 100644 --- a/Gamemodes/CaptureTheFlag.cs +++ b/Gamemodes/CaptureTheFlag.cs @@ -277,7 +277,7 @@ public static class CaptureTheFlag { PlayerControl pc = key.GetPlayer(); - if (pc != null && outfit != null) + if (pc && outfit != null) Utils.RpcChangeSkin(pc, outfit); } } diff --git a/Gamemodes/KingOfTheZones.cs b/Gamemodes/KingOfTheZones.cs index 5f5b63aa3..f8b322308 100644 --- a/Gamemodes/KingOfTheZones.cs +++ b/Gamemodes/KingOfTheZones.cs @@ -644,7 +644,7 @@ public static class KingOfTheZones } } - void ResetSkins() => DefaultOutfits.Select(x => (pc: x.Key.GetPlayer(), outfit: x.Value)).DoIf(x => x.pc != null && x.outfit != null, x => Utils.RpcChangeSkin(x.pc, x.outfit)); + void ResetSkins() => DefaultOutfits.Select(x => (pc: x.Key.GetPlayer(), outfit: x.Value)).DoIf(x => x.pc && x.outfit != null, x => Utils.RpcChangeSkin(x.pc, x.outfit)); } public static bool IsNotInLocalPlayersTeam(PlayerControl pc) @@ -897,7 +897,7 @@ public static class KingOfTheZones { if (tie) return; GameGoing = false; - DefaultOutfits.Select(x => (pc: x.Key.GetPlayer(), outfit: x.Value)).DoIf(x => x.pc != null && x.outfit != null, x => Utils.RpcChangeSkin(x.pc, x.outfit)); + DefaultOutfits.Select(x => (pc: x.Key.GetPlayer(), outfit: x.Value)).DoIf(x => x.pc && x.outfit != null, x => Utils.RpcChangeSkin(x.pc, x.outfit)); KOTZTeam winner = Points.GetKeyByValue(highestPoints); CustomWinnerHolder.WinnerIds = PlayerTeams.Where(x => x.Value == winner).Select(x => x.Key).ToHashSet(); Color color = winner.GetColor(); diff --git a/Gamemodes/SoloPVP.cs b/Gamemodes/SoloPVP.cs index 3f55a9bb0..9066a1b97 100644 --- a/Gamemodes/SoloPVP.cs +++ b/Gamemodes/SoloPVP.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using EHR.Modules; using Hazel; @@ -18,7 +19,8 @@ internal static class SoloPVP public static Dictionary PlayerDF = []; public static Dictionary PlayerScore = []; - public static int RoundTime; + public static Stopwatch RoundTimer; + public static int GameTime = 180; private static readonly Dictionary NameNotify = []; @@ -99,8 +101,14 @@ internal static class SoloPVP LastCountdownTime = []; BackCountdown = []; PlayerScore = []; - RoundTime = SoloPVP_GameTime.GetInt() + 8; - Utils.SendRPC(CustomRPC.SoloPVPSync, 1, RoundTime); + GameTime = SoloPVP_GameTime.GetInt(); + RoundTimer = Stopwatch.StartNew(); + LateTask.New(() => + { + if (RoundTimer.IsRunning) + RoundTimer.Restart(); + }, 3f); + Utils.SendRPC(CustomRPC.SoloPVPSync, 1); foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls()) { @@ -175,8 +183,6 @@ internal static class SoloPVP public static void GetNameNotify(PlayerControl player, ref string name) { - if (Options.CurrentGameMode != CustomGameMode.SoloPVP || player == null) return; - if (BackCountdown.TryGetValue(player.PlayerId, out int value)) { name = string.Format(Translator.GetString("SoloPVP_BackCountDown"), value); @@ -202,17 +208,13 @@ internal static class SoloPVP public static string GetHudText() { - if (RoundTime == 60) - { - SoundManager.Instance.PlaySound(HudManager.Instance.LobbyTimerExtensionUI.lobbyTimerPopUpSound, false); - Utils.FlashColor(new(1f, 1f, 0f, 0.4f), 1.4f); - } - return $"{RoundTime / 60:00}:{RoundTime % 60:00}"; + int roundTime = GameTime - (int)RoundTimer.Elapsed.TotalSeconds; + return $"{roundTime / 60:00}:{roundTime % 60:00}"; } public static void OnPlayerAttack(PlayerControl killer, PlayerControl target) { - if (killer == null || target == null || Options.CurrentGameMode != CustomGameMode.SoloPVP || !Main.IntroDestroyed) return; + if (!Main.IntroDestroyed) return; if (!killer.SoloAlive() || !target.SoloAlive() || target.inVent || target.MyPhysics.Animations.IsPlayingEnterVentAnimation()) return; @@ -228,7 +230,7 @@ internal static class SoloPVP LastHurt[target.PlayerId] = Utils.TimeStamp; float kcd = SoloPVP_ATKCooldown.GetFloat(); - if (killer.IsHost()) kcd += Math.Max(0.5f, Utils.CalculatePingDelay()); + if (killer.AmOwner) kcd += Math.Max(0.5f, Utils.CalculatePingDelay()); killer.SetKillCooldown(kcd, target); RPC.PlaySoundRPC(killer.PlayerId, Sounds.KillSound); @@ -333,8 +335,22 @@ internal static class SoloPVP if (LastFixedUpdate == now) return; LastFixedUpdate = now; - RoundTime--; - Utils.SendRPC(CustomRPC.SoloPVPSync, 1, RoundTime); + if (Main.AllAlivePlayerControls.Count <= 1) + { + if (ExtendedPlayerControl.TempExiled.Count == 0) + { + RoundTimer.Reset(); + return; + } + + // apart from 1 player, everyone is waiting for revival.... stop the timer temporarily + RoundTimer.Stop(); + } + else if (!RoundTimer.IsRunning) + { + // resume the timer + RoundTimer.Start(); + } Utils.NotifyRoles(SendOption: SendOption.None); } diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs index 457cbf81b..ddc376f14 100644 --- a/Modules/CustomNetObject.cs +++ b/Modules/CustomNetObject.cs @@ -96,7 +96,7 @@ namespace EHR try { - if (playerControl != null) + if (playerControl) { MessageWriter writer = MessageWriter.Get(SendOption.Reliable); writer.StartMessage(5); diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 52e6b94c6..688ba9e48 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -1206,7 +1206,7 @@ internal static class CustomRolesHelper CustomRoles.Bait when pc.Is(CustomRoles.Beartrap) => false, CustomRoles.Beartrap when pc.Is(CustomRoles.Bait) => false, CustomRoles.Schizophrenic when pc.Is(CustomRoles.Dictator) => false, - CustomRoles.Swift when pc.GetCustomRole() is CustomRoles.Swooper or CustomRoles.Vampire or CustomRoles.Scavenger or CustomRoles.Puppeteer or CustomRoles.Warlock or CustomRoles.Consigliere or CustomRoles.Witch or CustomRoles.Nemesis => false, + CustomRoles.Swift when pc.GetCustomRole() is CustomRoles.Swooper or CustomRoles.Vampire or CustomRoles.Scavenger or CustomRoles.Puppeteer or CustomRoles.Warlock or CustomRoles.Consigliere or CustomRoles.Witch or CustomRoles.Nemesis or CustomRoles.Viper or CustomRoles.ViperEHR => false, CustomRoles.Reach when pc.GetCustomRole() is CustomRoles.Mafioso or CustomRoles.Evolver or CustomRoles.Berserker => false, CustomRoles.Beartrap when pc.Is(CustomRoles.GuardianAngelEHR) => false, CustomRoles.Reach when pc.GetRoleTypes() is not (RoleTypes.Impostor or RoleTypes.Phantom or RoleTypes.Shapeshifter or RoleTypes.Viper) => false, diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index a6cd37aa3..4f53351e8 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -1502,7 +1502,7 @@ internal static class ExtendedPlayerControl return Options.CurrentGameMode switch { CustomGameMode.SoloPVP => SoloPVP.CanVent, - CustomGameMode.FFA => true, + CustomGameMode.FFA => !(FreeForAll.FFADisableVentingWhenKcdIsUp.GetBool() && Main.KillTimers[pc.PlayerId] <= 0) && !(FreeForAll.FFADisableVentingWhenTwoPlayersAlive.GetBool() && Main.AllAlivePlayerControls.Count <= 2), CustomGameMode.StopAndGo => false, CustomGameMode.HotPotato => false, CustomGameMode.Speedrun => false, diff --git a/Modules/RPC.cs b/Modules/RPC.cs index 7369fef22..aa539c4fe 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using AmongUs.GameOptions; using EHR.Gamemodes; @@ -1233,8 +1234,7 @@ internal static class RPCHandlerPatch switch (reader.ReadPackedInt32()) { case 1: - int roundTime = reader.ReadPackedInt32(); - SoloPVP.RoundTime = roundTime; + SoloPVP.RoundTimer = Stopwatch.StartNew(); break; case 2: SoloPVP.PlayerScore[reader.ReadByte()] = reader.ReadPackedInt32(); diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 26534583b..10326bc2d 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -3100,7 +3100,7 @@ public static class Utils string targetRoleText = KnowsTargetRole(seer, target) - ? $"{target.GetDisplayRoleName(seeTargetBetrayalAddons: shouldSeeTargetAddons)}{GetProgressText(target)}\r\n" + ? $"{GetRoleText(seer.PlayerId, target.PlayerId, seeTargetBetrayalAddons: shouldSeeTargetAddons)}{GetProgressText(target)}\r\n" : string.Empty; if (IsRevivingRoleAlive() && Main.DiedThisRound.Contains(seer.PlayerId)) @@ -3991,11 +3991,15 @@ public static class Utils Reaper.OnAnyoneDead(target); Wyrd.OnAnyoneDeath(target); Thanos.OnDeath(targetRealKiller, target, disconnect); - CovenMember.OnAnyoneDead(); - Altruist.OnAnyoneDead(); - Occultist.OnAnyoneDead(); - Vulture.OnAnyoneDead(); - Jackal.OnAnyoneDead(); + + LateTask.New(() => + { + CovenMember.OnAnyoneDead(); + Altruist.OnAnyoneDead(); + Occultist.OnAnyoneDead(); + Vulture.OnAnyoneDead(); + Jackal.OnAnyoneDead(); + }, 0.1f); if (!onMeeting && !disconnect) { @@ -4017,7 +4021,7 @@ public static class Utils Amogus.OnAnyoneDead(target); Adventurer.OnAnyoneDead(target); Whisperer.OnAnyoneDied(target); - Necromancer.OnAnyoneDead(); + LateTask.New(Necromancer.OnAnyoneDead, 0.1f); } if (QuizMaster.On) QuizMaster.Data.NumPlayersDeadThisRound++; diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index dc3d99117..6169d1811 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -200,7 +200,7 @@ internal static class ChatCommands new("EnableAllRoles", "", GetString("CommandDescription.EnableAllRoles"), Command.UsageLevels.Host, Command.UsageTimes.InLobby, EnableAllRolesCommand, true, false), new("Achievements", "", GetString("CommandDescription.Achievements"), Command.UsageLevels.Modded, Command.UsageTimes.Always, AchievementsCommand, true, false), new("DeathNote", "{name}", GetString("CommandDescription.DeathNote"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, DeathNoteCommand, true, true, [GetString("CommandArgs.DeathNote.Name")]), - new("Whisper", "{id} {message}", GetString("CommandDescription.Whisper"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, WhisperCommand, true, true, [GetString("CommandArgs.Whisper.Id"), GetString("CommandArgs.Whisper.Message")]), + new("Whisper", "{ids} {message}", GetString("CommandDescription.Whisper"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, WhisperCommand, true, true, [GetString("CommandArgs.Whisper.Ids"), GetString("CommandArgs.Whisper.Message")]), new("HWhisper", "{id} {message}", GetString("CommandDescription.HWhisper"), Command.UsageLevels.Host, Command.UsageTimes.Always, HWhisperCommand, true, false, [GetString("CommandArgs.HWhisper.Id"), GetString("CommandArgs.HWhisper.Message")]), new("Spectate", "[id]", GetString("CommandDescription.Spectate"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, SpectateCommand, false, false, [GetString("CommandArgs.Spectate.Id")]), new("Anagram", "", GetString("CommandDescription.Anagram"), Command.UsageLevels.Everyone, Command.UsageTimes.AfterDeathOrLobby, AnagramCommand, true, false), @@ -524,8 +524,18 @@ internal static class ChatCommands if (!Main.PlayerStates.TryGetValue(player.PlayerId, out PlayerState state) || state.IsDead || state.Role is not Summoner sum || player.GetAbilityUseLimit() < 1) return; if (args.Length < 2 || !byte.TryParse(args[1], out byte targetId) || !Main.PlayerStates.TryGetValue(targetId, out var targetState) || !targetState.IsDead) return; + bool reSummoned = !Summoner.AlreadySummoned.Add(targetId); + + if (reSummoned && !Summoner.AllowSummoningTheSamePlayerTwice.GetBool()) + { + Utils.SendMessage("\n", player.PlayerId, GetString("Summoner.CantSummonSamePlayerTwice")); + return; + } + sum.SummonedPlayerId = targetId; - player.RpcRemoveAbilityUse(); + + if (!reSummoned || Summoner.ReSummonTakesAbilityUse.GetBool()) + player.RpcRemoveAbilityUse(); Utils.SendMessage("\n", player.PlayerId, string.Format(GetString("Summoner.SummonSuccessMessage"), targetId.ColoredPlayerName())); @@ -1128,33 +1138,40 @@ internal static class ChatCommands return; } - if (args.Length < 3 || !byte.TryParse(args[1], out byte targetId)) return; - - PlayerState state = Main.PlayerStates[targetId]; - if (state.IsDead || state.SubRoles.Contains(CustomRoles.Shy)) return; - - string fromName = player.PlayerId.ColoredPlayerName(); - string toName = targetId.ColoredPlayerName(); - - string msg = args[2..].Join(delimiter: " "); - string title = string.Format(GetString("WhisperTitle"), fromName, player.PlayerId); - - Utils.SendMessage(msg, targetId, title, importance: MessageImportance.High); - ChatUpdatePatch.LastMessages.Add((msg, targetId, title, Utils.TimeStamp)); - - MeetingManager.SendCommandUsedMessage(args[0]); + if (args.Length < 3) return; string coloredRole = CustomRoles.Listener.ToColoredString(); + PlayerControl[] listeners = CustomRoles.Listener.IsEnable() ? Main.EnumerateAlivePlayerControls().Where(x => x.Is(CustomRoles.Listener)).ToArray() : []; + string[] ids = args[1].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - foreach (PlayerControl listener in Main.EnumerateAlivePlayerControls()) + foreach (string id in ids) { - if (!listener.Is(CustomRoles.Listener) || IRandom.Instance.Next(100) >= Listener.WhisperHearChance.GetInt()) continue; - string message = IRandom.Instance.Next(100) < Listener.FullMessageHearChance.GetInt() ? string.Format(GetString("Listener.FullMessage"), coloredRole, fromName, toName, msg) : string.Format(GetString("Listener.FromTo"), coloredRole, fromName, toName); - Utils.SendMessage("\n", listener.PlayerId, message); + if (!byte.TryParse(id, out byte targetId)) continue; - if (listener.AmOwner && ++Listener.LocalPlayerHeardMessagesThisMeeting >= 3) - Achievements.Type.Eavesdropper.Complete(); + PlayerState state = Main.PlayerStates[targetId]; + if (state.IsDead || state.SubRoles.Contains(CustomRoles.Shy)) continue; + + string fromName = player.PlayerId.ColoredPlayerName(); + string toName = targetId.ColoredPlayerName(); + + string msg = args[2..].Join(delimiter: " "); + string title = string.Format(GetString("WhisperTitle"), fromName, player.PlayerId); + + Utils.SendMessage(msg, targetId, title, importance: MessageImportance.High); + ChatUpdatePatch.LastMessages.Add((msg, targetId, title, Utils.TimeStamp)); + + foreach (PlayerControl listener in listeners) + { + if (IRandom.Instance.Next(100) >= Listener.WhisperHearChance.GetInt()) continue; + string message = IRandom.Instance.Next(100) < Listener.FullMessageHearChance.GetInt() ? string.Format(GetString("Listener.FullMessage"), coloredRole, fromName, toName, msg) : string.Format(GetString("Listener.FromTo"), coloredRole, fromName, toName); + Utils.SendMessage("\n", listener.PlayerId, message); + + if (listener.AmOwner && ++Listener.LocalPlayerHeardMessagesThisMeeting >= 3) + Achievements.Type.Eavesdropper.Complete(); + } } + + MeetingManager.SendCommandUsedMessage(args[0]); } private static void HWhisperCommand(PlayerControl player, string text, string[] args) diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index c92172522..c711c777f 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -791,7 +791,7 @@ internal static class GameEndChecker { reason = GameOverReason.ImpostorsByKill; - if (SoloPVP.RoundTime > 0) return false; + if (SoloPVP.RoundTimer.IsRunning && SoloPVP.RoundTimer.Elapsed.TotalSeconds < SoloPVP.GameTime) return false; HashSet winners = [Main.EnumeratePlayerControls().FirstOrDefault(x => !x.Is(CustomRoles.GM) && SoloPVP.GetRankFromScore(x.PlayerId) == 1)?.PlayerId ?? Main.EnumerateAlivePlayerControls().First().PlayerId]; int kills = SoloPVP.PlayerScore[winners.First()]; diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index d0db639d4..ee978424d 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -1176,7 +1176,7 @@ internal static class IntroCutsceneDestroyPatch case CustomGameMode.Quiz when Quiz.Chat: case CustomGameMode.HideAndSeek when CustomHnS.Chat: case CustomGameMode.NaturalDisasters when NaturalDisasters.Chat: - Utils.SetChatVisibleForAll(); + LateTask.New(Utils.SetChatVisibleForAll, 4f); break; } diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index ffe841b25..9770c390d 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -734,7 +734,7 @@ internal static class MeetingHudStartPatch LateTask.New(() => { PlayerControl player = Main.EnumerateAlivePlayerControls().MinBy(x => x.PlayerId); - var sender = CustomRpcSender.Create("RpcSetNameEx on meeting start", SendOption.Reliable); + var sender = CustomRpcSender.Create("Fix Sender Name", SendOption.Reliable); { sender.AutoStartRpc(player.NetId, 6); { @@ -745,7 +745,7 @@ internal static class MeetingHudStartPatch } sender.SendMessage(); }, 1f, "Fix Sender Name"); - }, 7f, "Send Role Descriptions Round 1"); + }, 8.5f, "Send Role Descriptions Round 1"); } if (Options.MadmateSpawnMode.GetInt() == 2 && CustomRoles.Madmate.IsEnable() && MeetingStates.FirstMeeting) @@ -792,7 +792,7 @@ internal static class MeetingHudStartPatch AddMsg(string.Format(GetString("SuperStarDead"), Main.AllPlayerNames[csId]), pc.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.SuperStar), GetString("SuperStarNewsTitle"))); } - if (pc != null && Silencer.ForSilencer.Contains(pc.PlayerId)) + if (Silencer.ForSilencer.Contains(pc.PlayerId)) { string playername = pc.GetRealName(); if (Doppelganger.DoppelVictim.TryGetValue(pc.PlayerId, out string value)) playername = value; @@ -834,7 +834,7 @@ internal static class MeetingHudStartPatch } } - if (msgToSend.Count > 0) LateTask.New(() => msgToSend.Do(x => Utils.SendMessage(x.Text, x.SendTo, x.Title, importance: MessageImportance.High)), 8f, "Meeting Start Notify"); + if (msgToSend.Count > 0) LateTask.New(() => msgToSend.Do(x => Utils.SendMessage(x.Text, x.SendTo, x.Title, importance: MessageImportance.High)), 9f, "Meeting Start Notify"); Main.SuperStarDead.Clear(); Forensic.ForensicNotify.Clear(); @@ -870,7 +870,7 @@ internal static class MeetingHudStartPatch foreach (PlayerVoteArea pva in __instance.playerStates) { PlayerControl target = Utils.GetPlayerById(pva.TargetPlayerId); - if (target == null) continue; + if (!target) continue; bool shouldSeeTargetAddons = seer.PlayerId == target.PlayerId || new[] { seer, target }.All(x => x.Is(Team.Impostor)); (string, Color) roleTextData = Utils.GetRoleText(seer.PlayerId, target.PlayerId, seeTargetBetrayalAddons: shouldSeeTargetAddons); @@ -954,7 +954,7 @@ internal static class MeetingHudStartPatch deathReasonTextMeeting.enableWordWrapping = false; deathReasonTextMeeting.enabled = seer.KnowDeathReason(target); Transform child = deathReasonTextMeeting.transform.FindChild("RoleTextMeeting"); - if (child != null) Object.Destroy(child.gameObject); + if (child) Object.Destroy(child.gameObject); byte id = pva.TargetPlayerId; @@ -992,7 +992,7 @@ internal static class MeetingHudStartPatch TemplateManager.SendTemplate("OnMeeting", noErr: true, importance: MessageImportance.Low); if (MeetingStates.FirstMeeting) TemplateManager.SendTemplate("OnFirstMeeting", noErr: true, importance: MessageImportance.Low); - }, 6.5f, log: false); + }, 8f, log: false); NotifyRoleSkillOnMeetingStart(); @@ -1045,10 +1045,10 @@ internal static class MeetingHudStartPatch foreach (PlayerVoteArea pva in __instance.playerStates) { - if (pva == null) continue; + if (!pva) continue; PlayerControl target = Utils.GetPlayerById(pva.TargetPlayerId); - if (target == null) continue; + if (!target) continue; if (seer.GetCustomRole().GetDYRole() is RoleTypes.Shapeshifter or RoleTypes.Phantom) { diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 9e5480c63..b7985ac68 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -653,7 +653,7 @@ internal static class MurderPlayerPatch { Infection.OnAnyMurder(); - Summoner.OnAnyoneMurder(killer); + Summoner.OnAnyoneMurder(killer, target); // Replacement process when the actual killer and killer are different if (Sniper.TryGetSniper(target.PlayerId, ref killer)) Main.PlayerStates[target.PlayerId].deathReason = PlayerState.DeathReason.Sniped; @@ -1041,7 +1041,7 @@ internal static class ReportDeadBodyPatch if (target == null) { - if (CustomSabotage.Instances.Count > 0) + if (IsAnySabotageActive()) { Notify("CannotCallEmergencyMeetingWhileSabotage"); return false; @@ -1114,6 +1114,11 @@ internal static class ReportDeadBodyPatch return false; } + if (!Summoner.OnAnyoneReport()) + { + Notify("SummonedPlayerAlive"); + } + if (!Wyrd.CheckPlayerAction(__instance, Wyrd.Action.Report)) return false; // Player dies, no notify needed PlayerControl tpc = target.Object; @@ -1200,14 +1205,17 @@ internal static class ReportDeadBodyPatch if (!Options.GameTimeLimitRunsDuringMeetings.GetBool()) Main.GameTimer.Stop(); - foreach (CustomNetObject cno in CustomNetObject.AllObjects.ToArray()) + LateTask.New(() => { - try + foreach (CustomNetObject cno in CustomNetObject.AllObjects.ToArray()) { - cno.OnMeeting(); + try + { + cno.OnMeeting(); + } + catch (Exception e) { ThrowException(e); } } - catch (Exception e) { ThrowException(e); } - } + }, 2f, "CNO OnMeeting"); if (HudManager.InstanceExists) HudManager.Instance.SetRolePanelOpen(false); @@ -1227,10 +1235,10 @@ internal static class ReportDeadBodyPatch foreach (byte id in Main.DiedThisRound) { PlayerControl receiver = id.GetPlayer(); - if (receiver == null) continue; + if (!receiver) continue; PlayerControl killer = receiver.GetRealKiller(); - if (killer == null) continue; + if (!killer) continue; SendMessage("\n", receiver.PlayerId, string.Format(GetString("DeathCommand"), killer.PlayerId.ColoredPlayerName(), (killer.Is(CustomRoles.Bloodlust) ? $"{CustomRoles.Bloodlust.ToColoredString()} " : string.Empty) + killer.GetCustomRole().ToColoredString()), importance: MessageImportance.Low); } @@ -1243,7 +1251,7 @@ internal static class ReportDeadBodyPatch try { Main.EnumerateAlivePlayerControls().DoIf(x => x.Is(CustomRoles.Lazy), x => Lazy.BeforeMeetingPositions[x.PlayerId] = x.Pos()); } catch (Exception e) { ThrowException(e); } - try { if (Lovers.PrivateChat.GetBool() && Main.LoversPlayers.Exists(x => x != null && x.IsAlive())) LateTask.New(() => ChatManager.ClearChat(Main.EnumerateAlivePlayerControls().ExceptBy(Main.LoversPlayers.ConvertAll(x => x.PlayerId), x => x.PlayerId).ToArray()), GameStates.CurrentServerType == GameStates.ServerType.Vanilla && !PlayerControl.LocalPlayer.IsAlive() ? 3f : 0f); } + try { if (Lovers.PrivateChat.GetBool() && Main.LoversPlayers.Exists(x => x && x.IsAlive())) LateTask.New(() => ChatManager.ClearChat(Main.EnumerateAlivePlayerControls().ExceptBy(Main.LoversPlayers.ConvertAll(x => x.PlayerId), x => x.PlayerId).ToArray()), GameStates.CurrentServerType == GameStates.ServerType.Vanilla && !PlayerControl.LocalPlayer.IsAlive() ? 1.5f : 0f); } catch (Exception e) { ThrowException(e); } CustomSabotage.Reset(); @@ -1272,7 +1280,7 @@ internal static class ReportDeadBodyPatch { PlayerControl tpc = target.Object; - if (tpc != null && !tpc.IsAlive()) + if (tpc && !tpc.IsAlive()) { if (player.Is(CustomRoles.Forensic) && player.PlayerId != target.PlayerId) Forensic.OnReportDeadBody(player, target.Object); @@ -1303,9 +1311,9 @@ internal static class ReportDeadBodyPatch QuizMaster.Data.NumMeetings++; } - if (Main.LoversPlayers.Exists(x => x != null && x.IsAlive()) && Main.IsLoversDead && Lovers.LoverDieConsequence.GetValue() == 1) + if (Main.IsLoversDead && Lovers.LoverDieConsequence.GetValue() == 1 && Main.LoversPlayers.Exists(x => x && x.IsAlive())) { - PlayerControl aliveLover = Main.LoversPlayers.First(x => x != null && x.IsAlive()); + PlayerControl aliveLover = Main.LoversPlayers.First(x => x && x.IsAlive()); switch (Lovers.LoverSuicideTime.GetValue()) { @@ -2184,14 +2192,6 @@ internal static class EnterVentPatch switch (Options.CurrentGameMode) { - case CustomGameMode.FFA when FreeForAll.FFADisableVentingWhenTwoPlayersAlive.GetBool() && Main.AllAlivePlayerControls.Count <= 2: - pc.Notify(GetString("FFA-NoVentingBecauseTwoPlayers"), 7f); - pc.MyPhysics?.RpcBootFromVent(__instance.Id); - break; - case CustomGameMode.FFA when FreeForAll.FFADisableVentingWhenKcdIsUp.GetBool() && Main.KillTimers[pc.PlayerId] <= 0: - pc.Notify(GetString("FFA-NoVentingBecauseKCDIsUP"), 7f); - pc.MyPhysics?.RpcExitVent(__instance.Id); - break; case CustomGameMode.HotPotato: case CustomGameMode.Speedrun: case CustomGameMode.CaptureTheFlag: diff --git a/Patches/SabotageSystemPatch.cs b/Patches/SabotageSystemPatch.cs index f10694c39..e3d3f2817 100644 --- a/Patches/SabotageSystemPatch.cs +++ b/Patches/SabotageSystemPatch.cs @@ -309,7 +309,7 @@ internal static class SabotageSystemTypeAnyActivePatch { public static bool Prefix(SabotageSystemType __instance, ref bool __result) { - __result = __instance.specials.ToArray().Any(s => s.IsActive) || CustomSabotage.Instances.Count > 0; + __result = __instance.specials.Exists((Il2CppSystem.Predicate)(s => s.IsActive)) || CustomSabotage.Instances.Count > 0; return false; } } diff --git a/Patches/TextBoxPatch.cs b/Patches/TextBoxPatch.cs index 9fc3b97e1..f5c14383e 100644 --- a/Patches/TextBoxPatch.cs +++ b/Patches/TextBoxPatch.cs @@ -188,7 +188,7 @@ public static class TextBoxPatch IsInvalidCommand = false; HudManager hud = HudManager.Instance; - if (PlaceHolderText == null) + if (!PlaceHolderText) { PlaceHolderText = Object.Instantiate(__instance.outputText, __instance.outputText.transform.parent); PlaceHolderText.name = "PlaceHolderText"; @@ -196,7 +196,7 @@ public static class TextBoxPatch PlaceHolderText.transform.localPosition = __instance.outputText.transform.localPosition; } - if (CommandInfoText == null) + if (!CommandInfoText) { CommandInfoText = Object.Instantiate(hud.KillButton.cooldownTimerText, hud.transform.parent, true); CommandInfoText.name = "CommandInfoText"; @@ -211,7 +211,7 @@ public static class TextBoxPatch CommandInfoText.transform.SetAsLastSibling(); } - if (AdditionalInfoText == null) + if (!AdditionalInfoText) { AdditionalInfoText = Object.Instantiate(hud.KillButton.cooldownTimerText, hud.transform.parent, true); AdditionalInfoText.name = "AdditionalInfoText"; @@ -284,6 +284,7 @@ public static class TextBoxPatch { "{uuid}" => arg.Length > 16, "{id}" or "{id1}" or "{id2}" => !byte.TryParse(arg, out byte id) || Main.EnumeratePlayerControls().All(x => x.PlayerId != id), + "{ids}" => arg.Split(',').Any(x => !byte.TryParse(x, out _)), "{number}" or "{level}" or "{duration}" or "{number1}" or "{number2}" => !int.TryParse(arg, out int num) || num < 0, "{team}" => arg is not "crew" and not "imp", "{role}" => !ChatCommands.GetRoleByName(arg, out _), @@ -299,6 +300,7 @@ public static class TextBoxPatch { "{sourcepreset}" or "{targetpreset}" => int.TryParse(arg, out int preset) && preset is >= 1 and <= 10, "{id}" or "{id1}" or "{id2}" => byte.TryParse(arg, out byte id) && Main.EnumeratePlayerControls().Any(x => x.PlayerId == id), + "{ids}" => arg.Split(',').All(x => byte.TryParse(x, out _)), "{team}" => arg is "crew" or "imp", "{role}" or "[role]" => ChatCommands.GetRoleByName(arg, out _), "{addon}" => ChatCommands.GetRoleByName(arg, out CustomRoles role) && role.IsAdditionRole(), @@ -313,6 +315,7 @@ public static class TextBoxPatch : argName switch { "{id}" or "{id1}" or "{id2}" => $" ({byte.Parse(arg).ColoredPlayerName()})", + "{ids}" => $" ({string.Join(", ", arg.Split(',').Select(x => byte.Parse(x).ColoredPlayerName()))})", "{role}" or "{addon}" or "[role]" when ChatCommands.GetRoleByName(arg, out CustomRoles role) => $" ({role.ToColoredString()})", "{color}" when ColorUtility.TryParseHtmlString($"#{arg}", out Color color) => $" ({Utils.ColorString(color, "COLOR")})", _ => string.Empty diff --git a/Resources/Config/EACList.txt b/Resources/Config/EACList.txt index e4af49f44..ee1c533cc 100644 --- a/Resources/Config/EACList.txt +++ b/Resources/Config/EACList.txt @@ -129,6 +129,8 @@ primesnow#4272 | 65b98a6de | HiverXM | Toxic | The n word todayside#9634 | 4e44cffe6 | | Racist | Joined a lobby with racial slur in name roundframe#4949 | 914830596 | PeaceMaker | Toxic | The n word setbud#4013 | 99b904495 | cursed | Toxic | The n word +jacksdeep#4118 | d5d97f4a8 | SIGMAMODE | Toxic | Extremely toxic player +dimbottle#0386 | 607326ed0 | Bob | Toxic | Alt of banned user SIGMAMODE ## Other tenthrope#3476 | | Oldishwire | Other | diff --git a/Resources/Lang/en_US.jsonc b/Resources/Lang/en_US.jsonc index d45601476..a3f8d384a 100644 --- a/Resources/Lang/en_US.jsonc +++ b/Resources/Lang/en_US.jsonc @@ -326,7 +326,7 @@ "AdrenalineCannotCallEmergencyMeeting": "As the <#ffff00>Adrenaline, you cannot call emergency meetings currently", "Adventurer": "Adventurer", "AdventurerInfo": "Collect Resources to Perform Actions", - "AdventurerInfoLong": "(Crewmates):\\nAs the Adventurer, you can collect resources listed below and craft them into weapons if you have the required amount of materials for it.\\n\\nResources (and how to obtain them):\\n- ❖ (Complete a Task)\\n- Θ (Players Leave This Behind*)\\n- ♳ (Dead Bodies*)\\n- ⁂ (Shapeshift Skins*)\\n- ☀ (Fix Lights)\\n- ○ (Found in Groups*)\\n* When this resource is available somewhere, you get an arrow pointing towards that location. (Except dead bodies.) Go there to collect it.\\n\\nWeapons (and their ingredients):\\n- Gun (1 ♳ + 1 ❖) (Kill nearest player)\\n- Shield (3 ❖) (Gives nearest player a 1-time shield)\\n- Lantern (1 ☀ + 1 ❖) (Increases crewmates' vision for some time)\\n- Portal (2 Θ) (Swaps you with a random player)\\n- Wrench (1 ☀ + 2 ❖) (Clears the current sabotage)\\n- Proxy (2 ○ + 1 ❖) (Gives you +1 vote in the next meeting)\\n- Prediction (1 ♳ + 2 ⁂ + 1 ❖) (Reveals the nearest player's role)\\n- RNG (2 Θ + 2 ❖) (Makes a random weapon)\\n\\nTo use your weapons, pet.", + "AdventurerInfoLong": "(Crewmates):\\nAs the Adventurer, you can collect resources listed below and craft them into weapons if you have the required amount of materials for it.\\n\\nResources (and how to obtain them):\\n- ❖ (Complete a Task)\\n- Θ (Players Leave This Behind*)\\n- ♳ (Dead Bodies*)\\n- ⁂ (Shapeshift Skins*)\\n- ☀ (Fix Lights)\\n- ○ (Found in Groups*)\\n* When this resource is available somewhere, you get an arrow pointing towards that location. (Except dead bodies.) Go there to collect it.\\n\\nWeapons (and their ingredients):\\n- Gun (1 ♳ + 1 ❖) (Kill nearest player)\\n- Shield (3 ❖) (Gives nearest player a 1-time shield)\\n- Lantern (1 ☀ + 1 ❖) (Increases crewmates' vision for some time)\\n- Portal (2 Θ) (Swaps you with a random player)\\n- Wrench (1 ☀ + 2 ❖) (Clears the current sabotage)\\n- Proxy (2 ○ + 1 ❖) (Gives you +1 vote in the next meeting)\\n- Prediction (1 ♳ + 2 ⁂ + 1 ❖) (Reveals the nearest player's role)\\n- RNG (2 Θ + 2 ❖) (Makes a random weapon)\\n\\nTo use your weapons, pet.", "AdventurerFound": "Found Resource: {0}", "AdventurerGun.Gun": "Gun", "AdventurerGun.Lantern": "Lantern", @@ -576,7 +576,7 @@ "CrusaderSkillLimit": "Maximum Crusades", "Dad": "Dad", "DadInfo": "You're that iconic dad....", - "DadInfoLong": "(Crewmates):\\nAs the dad, your life depends on alcohol. When you get attacked or voted out, you survive, but your alcohol decreases. Alcohol also decreases slowly over time. To increase your alcohol level, you have to get money from completing tasks and go to a random selected vent to buy a beer. Beer increases your alcohol level. When you finish all your tasks, you get new tasks, but you don't have to do them for task win. You don't know how much alcohol you have at the time. You only see warning when your alcohol level is low. Alcohol is counted in ‰.\\n\\nYou also have some abilities that are unlocked if you have enough alcohol. To use ability, switch between them using pet button and use selected one by venting:\\n- Going for milk: You go for milk. When you use it, you get teleported outside the map. You also can't discuss or vote in meetings. You can do nothing. You will come back with milk after exactly 10 years. When you come back, crewmates instantly win. When you use it, everyone will be informed about it.\\n- Super vision: You will have max vision for some time.\\n- Sniffing: You get arrows pointing to existing dead bodies after using it. Arrows only update when you sniff again.\\n- Sleep: During sleep people can't interact with you, but you can't see or move. During sleep you also cannot use any abilities and your alcohol is stable. You wake up when a meeting starts.\\n- Rage: You kill next person you see.\\n- Giving drink: You give all nearby players a drink. Every of these players will say their role in meeting with a chance of an incorrect result.\\n- Becoming god of alcohol: Resets your alcohol to 1‰, but your alcohol will increase instead of decreasing from now on.\\nEvery ability (except going for milk) will reduce your alcohol after using it.\\nIf your alcohol reaches 0‰, you die.\\nAlso, you see which abilities you can use. It can give you some more information about your alcohol level.\\nTo make this role balanced, when someone attacks you, you lose alcohol after some time and not instantly.", + "DadInfoLong": "(Crewmates):\\nAs the dad, your life depends on alcohol. When you get attacked or voted out, you survive, but your alcohol decreases. Alcohol also decreases slowly over time. To increase your alcohol level, you have to get money from completing tasks and go to a random selected vent to buy a beer. Beer increases your alcohol level. When you finish all your tasks, you get new tasks, but you don't have to do them for task win. You don't know how much alcohol you have at the time. You only see warning when your alcohol level is low. Alcohol is counted in ‰.\\n\\nYou also have some abilities that are unlocked if you have enough alcohol. To use ability, switch between them using pet button and use selected one by venting:\\n- Going for milk: You go for milk. When you use it, you get teleported outside the map. You also can't discuss or vote in meetings. You can do nothing. You will come back with milk after exactly 10 years. When you come back, crewmates instantly win. When you use it, everyone will be informed about it.\\n- Super vision: You will have max vision for some time.\\n- Sniffing: You get arrows pointing to existing dead bodies after using it. Arrows only update when you sniff again.\\n- Sleep: During sleep people can't interact with you, but you can't see or move. During sleep you also cannot use any abilities and your alcohol is stable. You wake up when a meeting starts.\\n- Rage: You kill next person you see.\\n- Giving drink: You give all nearby players a drink. Every of these players will say their role in meeting with a chance of an incorrect result.\\n- Becoming god of alcohol: Resets your alcohol to 1‰, but your alcohol will increase instead of decreasing from now on.\\nEvery ability (except going for milk) will reduce your alcohol after using it.\\nIf your alcohol reaches 0‰, you die.\\nAlso, you see which abilities you can use. It can give you some more information about your alcohol level.\\nTo make this role balanced, when someone attacks you, you lose alcohol after some time and not instantly.", "Dad.Ability.BecomeGodOfAlcohol": "Becoming God Of Alcohol", "Dad.Ability.GiveDrink": "Giving Drink", "Dad.Ability.GoForMilk": "Going For Milk", @@ -985,7 +985,7 @@ "notifyJailedOnMeeting": "Notify Jailed player when meeting starts", "Journalist": "Journalist", "JournalistInfo": "Write Down Notes", - "JournalistInfoLong": "(Crewmates):\\nAs the Journalist, you can write down notes. When you die, all players will see your notes in the next meeting.\\n\\nCommand: /cmd note {add/remove/clear/view} [value]\\n- add: Add a note specified in [value]\\n- remove: Remove a note. Provide the index of it in [value] or leave [value] blank to remove the last note. You can see the indexes of your notes by using /cmd note view\\n- clear: Clear all notes (leave [value] blank)\\n- view: View all notes and their indexes (leave [value] blank)\\n\\nExample uses:\\n/cmd note add I think blue and yellow are Lovers\\n/cmd note remove 2\\n/cmd note clear\\n/cmd note view", + "JournalistInfoLong": "(Crewmates):\\nAs the Journalist, you can write down notes. When you die, all players will see your notes in the next meeting.\\n\\nCommand: /cmd note {add/remove/clear/view} [value]\\n- add: Add a note specified in [value]\\n- remove: Remove a note. Provide the index of it in [value] or leave [value] blank to remove the last note. You can see the indexes of your notes by using /cmd note view\\n- clear: Clear all notes (leave [value] blank)\\n- view: View all notes and their indexes (leave [value] blank)\\n\\nExample uses:\\n/cmd note add I think blue and yellow are Lovers\\n/cmd note remove 2\\n/cmd note clear\\n/cmd note view", "Journalist.CommandUsage": "Usage: /cmd note {add/remove/clear/view} [value]", "JournalistError.ArgumentOutOfRange": "The index you provided is out of range. Use /cmd note view to see the indexes of your notes.", "JournalistError.IndexOutOfRange": "You didn't provide enough arguments.", @@ -1731,7 +1731,7 @@ "ClockBlocker.MaxEmergencyCooldown": "Maximum Emergency Cooldown", "Commander": "Commander", "CommanderInfo": "Command your team", - "CommanderInfoLong": "(Impostors):\\nAs the Commander, you can switch between 6 modes by venting/petting. You can execute the selected action by shapeshifting.\\n\\nMark: Marks your shapeshift target with a unique color visible to all impostors. Use this to command them to kill that player!\\n\\nWhistle: This tells all or 1 specific* impostor(s) to come to you and gives them (each) an arrow pointing towards you.\\n\\nKill Anyone: Notifies all or 1 specific* impostor(s) to kill anyone.\\n\\nLeave Player Alive: Shapeshifting into a crewmate marks that player with a unique color and text telling other impostors to not kill that player. Shapeshifting into an impostor tells that impostor to not kill anyone.\\n\\nDon't Sabotage: Notifies all impostors to not call any sabotage.\\n\\nUse Ability: Notifies all or 1 specific* impostor(s) to use their ability.\\n\\n* If you shapeshift into an impostor, the action executes specifically and only on them.", + "CommanderInfoLong": "(Impostors):\\nAs the Commander, you can switch between 6 modes by venting/petting. You can execute the selected action by shapeshifting.\\n\\nMark: Marks your shapeshift target with a unique color visible to all impostors. Use this to command them to kill that player!\\n\\nWhistle: This tells all or 1 specific* impostor(s) to come to you and gives them (each) an arrow pointing towards you.\\n\\nKill Anyone: Notifies all or 1 specific* impostor(s) to kill anyone.\\n\\nLeave Player Alive: Shapeshifting into a crewmate marks that player with a unique color and text telling other impostors to not kill that player. Shapeshifting into an impostor tells that impostor to not kill anyone.\\n\\nDon't Sabotage: Notifies all impostors to not call any sabotage.\\n\\nUse Ability: Notifies all or 1 specific* impostor(s) to use their ability.\\n\\n* If you shapeshift into an impostor, the action executes specifically and only on them.", "CommanderDontKill": "Don't kill this player", "CommanderDontKillAnyoneNotify": "Commander wants you to not kill", "CommanderDontKillMarkMode": "Mark a Player to Leave Alive", @@ -2630,7 +2630,7 @@ "FollowerMaxBetTimes": "Maximum Number of Follows", "Gaslighter": "Gaslighter", "GaslighterInfo": "Make others doubt their own memory", - "GaslighterInfoLong": "(Neutrals):\\nAs the Gaslighter, you do different actions through your kill button every round.\\n\\nRound 1: Normal kills\\nRound 2: Knight players\\nRound 3: Curse players (like Witch)\\nRound 4: Shield players\\n\\nThen the cycle repeats, or you stay on normally killing until the end of the game depending on the settings.\\nSettings determine your win condition.", + "GaslighterInfoLong": "(Neutrals):\\nAs the Gaslighter, you do different actions through your kill button every round.\\n\\nRound 1: Normal kills\\nRound 2: Knight players\\nRound 3: Curse players (like Witch)\\nRound 4: Shield players\\n\\nThen the cycle repeats, or you stay on normally killing until the end of the game depending on the settings.\\nSettings determine your win condition.", "Gaslighter.Curse": "In this round, you <#ff00ff>curse players", "Gaslighter.CycleRepeats": "Kill Button Actions Cycle Repeats", "Gaslighter.Kill": "In this round, you <#ff1919>kill normally", @@ -3404,11 +3404,22 @@ "SummonerInfo": "Summon Players to Kill For You", "SummonerInfoLong": "(Coven):\\nUse /cmd summon {player ID} (or the kick menu) to select a dead player to revive (summon) after the meeting. They will have a certain time to kill for you. If they succeed, they can also win alongside you, as well as their original team. Otherwise they die again at the end of the time limit, if they aren't already. With the Necronomicon, you gain a kill button and the ability to kill normally.\\n\\nType /cmd id to get a list of player IDs, or view them next to the player names on the meeting screen. Example use: /cmd summon 5", "Summoner.SummonedTimeToKill": "Time To Kill For Summoned Players", + "Summoner.SummonDelayAfterMeeting": "Summon Delay After Meetings", + "Summoner.SummonedKillCooldown": "Summoned Player's Kill Cooldown", + "Summoner.SummonedKnowsCoven": "Summoned Players Know Coven Members", + "Summoner.PlayersSeeSummonedPlayerWarning": "Players See Who The Summoned Player Is", + "Summoner.AllowSummoningTheSamePlayerTwice": "Allow Summoning The Same Player Twice", + "Summoner.ReSummonTakesAbilityUse": "Re-Summoning Takes Ability Use", + "Summoner.SummonedKillsCountForSummoner": "Players who were killed by Summoned Players have the <#7b3fbb>Summoner as their Real Killer", + "Summoner.BlockMeetingsWhileSummonedPlayerAlive": "Block Meetings While a Summoned Player Is Alive", "Summoner.SomeoneWillBeRevivedMessage": "Someone from the dead has been chosen to kill for the Coven.", "Summoner.SelfSuffix": "{0} has {1}s to kill", "Summoner.SummonedPlayerSuffix": "<#ffff00>{0}, the {1} summoned you to kill for them!\\nYou have {2}s", + "Summoner.SummonedWarningSuffix": "Summoned to kill for the Coven", "Summoner.SummonSuccessMessage": "<#00ff00>✓ <#00bb00>{0} will be summoned after the meeting to kill for you", + "Summoner.CantSummonSamePlayerTwice": "<#ff5555>X <#bb0000>The same player cannot be summoned twice", "SummonerCantKillCoven": "As a summoned player, you kill for the Coven, meaning you can't kill Coven members", + "SummonedPlayerAlive": "Meetings can't be started while a summoned player is alive", "Timelord": "Timelord", "TimelordInfo": "Refresh or reset cooldowns; kill", "TimelordInfoLong": "(Coven):\\nUse your ability on a Coven member to refresh their cooldown (set it to 0). Use your ability on a non-Coven member to reset their cooldown. When enhanced by the Necronomicon, you can kill and also reduce meeting time upon making a kill.", @@ -8885,7 +8896,7 @@ "CommandArgs.Unmute.Id": "The player's ID to unmute", "CommandArgs.Up.Role": "The role to set", "CommandArgs.Vote.Id": "The player's ID to vote for", - "CommandArgs.Whisper.Id": "The player's ID to whisper to", + "CommandArgs.Whisper.Ids": "The players' IDs to whisper to, separated by commas (no space)", "CommandArgs.Whisper.Message": "The message to whisper", "CommandArgs.XOR.Role": "A main role that will never spawn in the same game with the other role", // Command Descriptions @@ -8990,7 +9001,7 @@ "CommandDescription.Version": "View the current version of EHR", "CommandDescription.Vote": "Vote for a player", "CommandDescription.VoteStart": "Vote to start the game", - "CommandDescription.Whisper": "Whisper a message to a player.", + "CommandDescription.Whisper": "Whisper a message privately to the specified players.", "CommandDescription.Winner": "View the winners of the last game", "CommandDescription.XOR": "Specify 2 roles that will never spawn in the same game.", // Command Forms diff --git a/Roles/Standard/Coven/CovenMember.cs b/Roles/Standard/Coven/CovenMember.cs index 22bc5b5a1..75d225b54 100644 --- a/Roles/Standard/Coven/CovenMember.cs +++ b/Roles/Standard/Coven/CovenMember.cs @@ -45,7 +45,7 @@ public class CovenMember : CovenBase foreach (CovenMember instance in Instances) { PlayerControl pc = Utils.GetPlayerById(instance.CovenMemberId); - if (pc == null || !pc.IsAlive()) continue; + if (!pc || !pc.IsAlive()) continue; pc.RpcSetCustomRole(CustomRoles.CovenLeader); pc.RpcChangeRoleBasis(CustomRoles.CovenLeader); pc.MarkDirtySettings(); diff --git a/Roles/Standard/Coven/Summoner.cs b/Roles/Standard/Coven/Summoner.cs index b67a26e03..f84c9a52b 100644 --- a/Roles/Standard/Coven/Summoner.cs +++ b/Roles/Standard/Coven/Summoner.cs @@ -12,7 +12,16 @@ public class Summoner : CovenBase public static bool On; public static List Instances = []; public static HashSet AdditionalWinners = []; + public static HashSet AlreadySummoned = []; + private static OptionItem SummonDelayAfterMeeting; + private static OptionItem SummonedKillCooldown; + private static OptionItem SummonedKnowsCoven; + private static OptionItem PlayersSeeSummonedPlayerWarning; + public static OptionItem AllowSummoningTheSamePlayerTwice; + public static OptionItem ReSummonTakesAbilityUse; + private static OptionItem SummonedKillsCountForSummoner; + private static OptionItem BlockMeetingsWhileSummonedPlayerAlive; private static OptionItem SummonedTimeToKill; private static OptionItem AbilityUseLimit; private static OptionItem AbilityUseGainWithEachKill; @@ -34,6 +43,14 @@ public class Summoner : CovenBase public override void SetupCustomOption() { StartSetup(658000) + .AutoSetupOption(ref SummonDelayAfterMeeting, 10f, new FloatValueRule(0f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref SummonedKillCooldown, 15f, new FloatValueRule(0f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref SummonedKnowsCoven, true) + .AutoSetupOption(ref PlayersSeeSummonedPlayerWarning, true) + .AutoSetupOption(ref AllowSummoningTheSamePlayerTwice, true) + .AutoSetupOption(ref ReSummonTakesAbilityUse, true, overrideParent: AllowSummoningTheSamePlayerTwice) + .AutoSetupOption(ref SummonedKillsCountForSummoner, true) + .AutoSetupOption(ref BlockMeetingsWhileSummonedPlayerAlive, true) .AutoSetupOption(ref SummonedTimeToKill, 30f, new FloatValueRule(0f, 120f, 0.5f), OptionFormat.Seconds) .AutoSetupOption(ref AbilityUseLimit, 1f, new FloatValueRule(0, 20, 0.05f), OptionFormat.Times) .AutoSetupOption(ref AbilityUseGainWithEachKill, 0.3f, new FloatValueRule(0f, 5f, 0.1f), OptionFormat.Times) @@ -49,6 +66,7 @@ public class Summoner : CovenBase On = false; Instances = []; AdditionalWinners = []; + AlreadySummoned = []; } public override void Add(byte playerId) @@ -91,22 +109,34 @@ public class Summoner : CovenBase return HasNecronomicon && CanVentAfterNecronomicon.GetBool(); } + public static bool OnAnyoneReport() + { + return !Instances.Exists(x => x.SummonedPlayerId != byte.MaxValue); + } + public override void AfterMeetingTasks() { - if (SummonedPlayerId != byte.MaxValue && Main.PlayerStates.TryGetValue(SummonedPlayerId, out var state)) + LateTask.New(() => { - var summoned = SummonedPlayerId.GetPlayer(); + if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks) return; + if (SummonedPlayerId == byte.MaxValue || !Main.PlayerStates.TryGetValue(SummonedPlayerId, out var state)) return; - if (summoned != null && !summoned.IsAlive()) + PlayerControl summoned = SummonedPlayerId.GetPlayer(); + + if (summoned && !summoned.IsAlive()) { SummonedPlayerTimer = new(SummonedTimeToKill.GetFloat(), () => { SummonedPlayerTimer = null; SummonedPlayerId = byte.MaxValue; - if (summoned == null || !summoned.IsAlive()) return; + if (!summoned || !summoned.IsAlive()) return; state.SetDead(); summoned.RpcExileV2(); - }, onTick: () => Utils.NotifyRoles(SpecifySeer: summoned, SpecifyTarget: summoned), onCanceled: () => SummonedPlayerTimer = null); + }, onTick: () => Utils.NotifyRoles(SpecifySeer: summoned, SpecifyTarget: summoned), onCanceled: () => + { + SummonedPlayerTimer = null; + SummonedPlayerId = byte.MaxValue; + }); RPC.PlaySoundRPC(SummonedPlayerId, Sounds.SpawnSound); GhostRolesManager.RemoveGhostRole(SummonedPlayerId); @@ -116,18 +146,18 @@ public class Summoner : CovenBase summoned.RpcChangeRoleBasis(CustomRoles.SerialKiller); summoned.SyncGeneralOptions(); summoned.SyncSettings(); - summoned.TP(FastVector2.TryGetClosestPlayerTo(summoned, out PlayerControl closest) ? closest : PlayerControl.LocalPlayer); - LateTask.New(() => summoned.SetKillCooldown(10f), 0.2f); + summoned.TPToRandomVent(); + LateTask.New(() => summoned.SetKillCooldown(SummonedKillCooldown.GetFloat()), 0.2f); Utils.SendRPC(CustomRPC.SyncRoleData, SummonerId, 1, SummonedPlayerId); } - } + }, Math.Max(0, SummonDelayAfterMeeting.GetFloat() - 2f), "Summon Delay"); if (!HasNecronomicon || Changed) return; var summoner = SummonerId.GetPlayer(); - if (summoner == null || !summoner.IsAlive()) return; + if (!summoner || !summoner.IsAlive()) return; summoner.RpcChangeRoleBasis(CustomRoles.SerialKiller); summoner.ResetKillCooldown(); @@ -145,7 +175,7 @@ public class Summoner : CovenBase public override bool KnowRole(PlayerControl seer, PlayerControl target) { if (base.KnowRole(seer, target)) return true; - return seer.PlayerId == SummonedPlayerId && target.Is(Team.Coven); + return SummonedKnowsCoven.GetBool() && seer.PlayerId == SummonedPlayerId && target.Is(Team.Coven); } public static bool OnAnyoneCheckMurder(PlayerControl killer, PlayerControl target) @@ -153,7 +183,7 @@ public class Summoner : CovenBase return !Instances.Exists(x => x.SummonedPlayerId == killer.PlayerId && target.Is(Team.Coven)); } - public static void OnAnyoneMurder(PlayerControl killer) + public static void OnAnyoneMurder(PlayerControl killer, PlayerControl target) { foreach (Summoner instance in Instances) { @@ -166,6 +196,7 @@ public class Summoner : CovenBase Main.PlayerStates[instance.SummonedPlayerId].SetDead(); instance.SummonedPlayerId = byte.MaxValue; Utils.SendRPC(CustomRPC.SyncRoleData, instance.SummonerId, 2); + if (SummonedKillsCountForSummoner.GetBool()) target.SetRealKiller(instance.SummonerId.GetPlayer()); return; } } @@ -199,7 +230,9 @@ public class Summoner : CovenBase public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) { - if (seer.PlayerId != target.PlayerId || (seer.IsModdedClient() && !hud) || meeting || SummonedPlayerId == byte.MaxValue || SummonedPlayerTimer == null) return string.Empty; + if (meeting || SummonedPlayerId == byte.MaxValue || SummonedPlayerTimer == null) return string.Empty; + if (target.PlayerId == SummonedPlayerId && seer.PlayerId != target.PlayerId && PlayersSeeSummonedPlayerWarning.GetBool()) return Utils.ColorString(Utils.GetRoleColor(CustomRoles.Summoner), Translator.GetString("Summoner.SummonedWarningSuffix")); + if (seer.PlayerId != target.PlayerId || (seer.IsModdedClient() && !hud)) return string.Empty; if (seer.PlayerId == SummonerId) return string.Format(Translator.GetString("Summoner.SelfSuffix"), SummonedPlayerId.ColoredPlayerName(), (int)SummonedPlayerTimer.Remaining.TotalSeconds); return seer.PlayerId == SummonedPlayerId ? string.Format(Translator.GetString("Summoner.SummonedPlayerSuffix"), SummonerId.ColoredPlayerName(), CustomRoles.Summoner.ToColoredString(), (int)SummonedPlayerTimer.Remaining.TotalSeconds) : string.Empty; } diff --git a/Roles/Standard/Crewmate/Support/Altruist.cs b/Roles/Standard/Crewmate/Support/Altruist.cs index 781639652..712f56bc7 100644 --- a/Roles/Standard/Crewmate/Support/Altruist.cs +++ b/Roles/Standard/Crewmate/Support/Altruist.cs @@ -128,7 +128,7 @@ public class Altruist : RoleBase { PlayerControl revivedPlayer = revived.GetPlayer(); - if (revivedPlayer == null || !revivedPlayer.IsAlive()) + if (!revivedPlayer || !revivedPlayer.IsAlive()) { TargetArrow.Remove(pc.PlayerId, revived); if (GameStates.IsMeeting || ExileController.Instance) continue; diff --git a/Roles/Standard/Impostor/Killing/Trapster.cs b/Roles/Standard/Impostor/Killing/Trapster.cs index 722f5fdf1..56b431e5d 100644 --- a/Roles/Standard/Impostor/Killing/Trapster.cs +++ b/Roles/Standard/Impostor/Killing/Trapster.cs @@ -138,7 +138,8 @@ internal class Trapster : RoleBase { Vector2 location = player.Pos(); TrapsterBody.Add(player.PlayerId); - Utils.RpcCreateDeadBody(location, (byte)IRandom.Instance.Next(17), player); + PlayerControl randomPlayer = Main.EnumerateAlivePlayerControls().Without(player).RandomElement(); + Utils.RpcCreateDeadBody(location, (byte)randomPlayer.CurrentOutfit.ColorId, randomPlayer); return false; } diff --git a/Roles/Standard/Neutral/Evil/Vulture.cs b/Roles/Standard/Neutral/Evil/Vulture.cs index 5a5f69eeb..0569e8004 100644 --- a/Roles/Standard/Neutral/Evil/Vulture.cs +++ b/Roles/Standard/Neutral/Evil/Vulture.cs @@ -198,7 +198,7 @@ public class Vulture : RoleBase if (!Main.PlayerStates.TryGetValue(x, out PlayerState state) || state.Role is not Vulture vulture) return; var pc = x.GetPlayer(); - if (pc == null || !pc.IsAlive()) return; + if (!pc || !pc.IsAlive()) return; if (ChangeRoleWhenCantWin.GetBool() && Main.AllAlivePlayerControls.Count - 1 <= NumberOfReportsToWin.GetInt() - vulture.TotalEaten) { diff --git a/Roles/Standard/Neutral/Killing/Jackal.cs b/Roles/Standard/Neutral/Killing/Jackal.cs index 5daeac72c..ed0916c39 100644 --- a/Roles/Standard/Neutral/Killing/Jackal.cs +++ b/Roles/Standard/Neutral/Killing/Jackal.cs @@ -196,7 +196,7 @@ public class Jackal : RoleBase { if (!CanRecruitImpostors.GetBool() && pc.Is(CustomRoleTypes.Impostor)) return false; if (!CanRecruitMadmates.GetBool() && pc.IsMadmate()) return false; - return pc != null && !pc.Is(CustomRoles.Sidekick) && !pc.Is(CustomRoles.Curser) && !pc.Is(CustomRoles.Loyal) && !pc.Is(CustomRoles.Bloodlust) && !pc.IsConverted() && pc.GetCustomRole().IsAbleToBeSidekicked(); + return pc && !pc.Is(CustomRoles.Sidekick) && !pc.Is(CustomRoles.Curser) && !pc.Is(CustomRoles.Loyal) && !pc.Is(CustomRoles.Bloodlust) && !pc.IsConverted() && pc.GetCustomRole().IsAbleToBeSidekicked(); } public void PromoteSidekick() @@ -206,7 +206,7 @@ public class Jackal : RoleBase if (!SKPromotesToJackal.GetBool()) return; PlayerControl sk = SidekickId.GetPlayer(); - if (sk == null || !sk.Is(CustomRoles.Sidekick)) return; + if (!sk || !sk.Is(CustomRoles.Sidekick)) return; sk.RpcSetCustomRole(CustomRoles.Jackal); if (!PromotedSKCanRecruit.GetBool()) sk.SetAbilityUseLimit(0); @@ -219,7 +219,7 @@ public class Jackal : RoleBase Instances.ForEach(x => { var pc = x.JackalId.GetPlayer(); - if (pc == null || pc.IsAlive()) return; + if (!pc || pc.IsAlive()) return; x.PromoteSidekick(); }); diff --git a/Roles/Standard/Neutral/Killing/Necromancer.cs b/Roles/Standard/Neutral/Killing/Necromancer.cs index 6037d4c8a..c8c6efe48 100644 --- a/Roles/Standard/Neutral/Killing/Necromancer.cs +++ b/Roles/Standard/Neutral/Killing/Necromancer.cs @@ -138,7 +138,7 @@ internal class Necromancer : RoleBase { if (!GameStates.IsInTask || ExileController.Instance) return; - if ((NecromancerPC == null || !NecromancerPC.IsAlive()) && (Deathknight.DeathknightPC != null && Deathknight.DeathknightPC.IsAlive())) + if ((!NecromancerPC || !NecromancerPC.IsAlive()) && (Deathknight.DeathknightPC && Deathknight.DeathknightPC.IsAlive())) { Deathknight.DeathknightPC.RpcSetCustomRole(CustomRoles.Necromancer); Deathknight.DeathknightPC = null; @@ -146,7 +146,7 @@ internal class Necromancer : RoleBase return; } - if (!CustomRoles.Deathknight.RoleExist() && (Deathknight.DeathknightId != byte.MaxValue || Deathknight.DeathknightPC != null)) + if (!CustomRoles.Deathknight.RoleExist() && (Deathknight.DeathknightId != byte.MaxValue || Deathknight.DeathknightPC)) { Deathknight.DeathknightPC = null; Deathknight.DeathknightId = byte.MaxValue;