diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 03bb2ce6b..d6287be76 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -760,7 +760,7 @@ internal static class ExtendedPlayerControl LateTask.New(() => { Main.PlayerStates[player.PlayerId].IsBlackOut = false; // Cancel blackout - player.SyncSettings(); + player.MarkDirtySettings(); }, duration, "RemoveKillFlash"); if (player.AmOwner) @@ -773,9 +773,9 @@ internal static class ExtendedPlayerControl MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.KillFlash, SendOption.Reliable, player.OwnerId); AmongUsClient.Instance.FinishRpcImmediately(writer); } - else if (!reactorCheck) player.ReactorFlash(); // Reactor flash + else if (!reactorCheck) player.ReactorFlash(canBlind: false); // Reactor flash - player.SyncSettings(); + player.MarkDirtySettings(); } public static void RpcGuardAndKill(this PlayerControl killer, PlayerControl target = null, bool forObserver = false, bool fromSetKCD = false) @@ -1229,7 +1229,7 @@ internal static class ExtendedPlayerControl }, 1f + (AmongUsClient.Instance.Ping / 1000f), log: false); } - public static void ReactorFlash(this PlayerControl pc, float delay = 0f, float flashDuration = float.NaN) + public static void ReactorFlash(this PlayerControl pc, float delay = 0f, float flashDuration = float.NaN, bool canBlind = true) { if (pc == null) return; @@ -1242,15 +1242,15 @@ internal static class ExtendedPlayerControl _ => SystemTypes.Reactor }; - if (IsActive(systemtypes)) + if (canBlind && IsActive(systemtypes)) { Main.PlayerStates[pc.PlayerId].IsBlackOut = true; - pc.SyncSettings(); + pc.MarkDirtySettings(); LateTask.New(() => { Main.PlayerStates[pc.PlayerId].IsBlackOut = false; - pc.SyncSettings(); + pc.MarkDirtySettings(); }, (float.IsNaN(flashDuration) ? Options.KillFlashDuration.GetFloat() : flashDuration) + delay, "Fix BlackOut Reactor Flash"); return; diff --git a/Modules/FixedUpdateCaller.cs b/Modules/FixedUpdateCaller.cs index 81949dce1..13d8d75b6 100644 --- a/Modules/FixedUpdateCaller.cs +++ b/Modules/FixedUpdateCaller.cs @@ -214,15 +214,18 @@ public static class FixedUpdateCaller try { - Main.GameTimer += Time.fixedDeltaTime; - - if (Options.EnableGameTimeLimit.GetBool() && Main.GameTimer > Options.GameTimeLimit.GetInt() && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.NaturalDisasters) + if (Options.EnableGameTimeLimit.GetBool()) { - Main.GameTimer = 0f; - CustomWinnerHolder.ResetAndSetWinner(CustomWinner.None); + Main.GameTimer += Time.fixedDeltaTime; - if (Options.CurrentGameMode == CustomGameMode.NaturalDisasters) - CustomWinnerHolder.WinnerIds.UnionWith(Main.AllAlivePlayerControls.Select(x => x.PlayerId)); + if (Main.GameTimer > Options.GameTimeLimit.GetInt() && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.NaturalDisasters) + { + Main.GameTimer = 0f; + CustomWinnerHolder.ResetAndSetWinner(CustomWinner.None); + + if (Options.CurrentGameMode == CustomGameMode.NaturalDisasters) + CustomWinnerHolder.WinnerIds.UnionWith(Main.AllAlivePlayerControls.Select(x => x.PlayerId)); + } } } catch (Exception e) { Utils.ThrowException(e); } diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs index cf82be548..9e6c471a7 100644 --- a/Modules/GuessManager.cs +++ b/Modules/GuessManager.cs @@ -1116,6 +1116,7 @@ public static class GuessManager private static bool CanGuess(PlayerControl lp, bool restrictions) { + if (Banshee.Instances.Exists(x => x.ScreechedPlayers.Contains(lp.PlayerId))) return false; // Vanilla clients can't guess with their chat hidden, so don't let modded clients guess for fairness return lp.Is(CustomRoles.Guesser) || lp.GetCustomRole() switch { CustomRoles.EvilGuesser => true, diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index 1a81a7f80..5556e069a 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -598,7 +598,7 @@ internal static class GameEndChecker foreach (PlayerState playerState in statesCoutingAsCrew) { - if (playerState.Player == null || !playerState.Player.IsAlive() || !Options.CrewAdvancedGameEndCheckingSettings.TryGetValue(playerState.MainRole, out var option) || !option.GetBool()) continue; + if (playerState.IsDead || !Options.CrewAdvancedGameEndCheckingSettings.TryGetValue(playerState.MainRole, out var option) || !option.GetBool()) continue; playerState.Role.ManipulateGameEndCheckCrew(out bool keepGameGoing, out int countsAs); crewKeepsGameGoing |= keepGameGoing; crew += countsAs - 1; diff --git a/Patches/ControlPatch.cs b/Patches/ControlPatch.cs index b0b88036a..74490d031 100644 --- a/Patches/ControlPatch.cs +++ b/Patches/ControlPatch.cs @@ -86,15 +86,11 @@ internal static class ControllerManagerUpdatePatch if (KeysDown(KeyCode.LeftAlt, KeyCode.C) && !Input.GetKey(KeyCode.LeftShift) && !GameStates.IsNotJoined) Utils.CopyCurrentSettings(); - - bool admin = ChatCommands.IsPlayerAdmin(PlayerControl.LocalPlayer.FriendCode); - if (!AmongUsClient.Instance.AmHost && !admin) return; - - if (KeysDown(KeyCode.Return, KeyCode.C, KeyCode.LeftShift)) + if (KeysDown(KeyCode.Return, KeyCode.C, KeyCode.LeftShift) && ChatCommands.IsPlayerAdmin(PlayerControl.LocalPlayer.FriendCode)) HudManager.Instance.Chat.SetVisible(true); - if (KeysDown(KeyCode.Return, KeyCode.L, KeyCode.LeftShift) && GameStates.IsInGame) + if (KeysDown(KeyCode.Return, KeyCode.L, KeyCode.LeftShift) && GameStates.IsInGame && ChatCommands.IsPlayerAdmin(PlayerControl.LocalPlayer.FriendCode)) { CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Draw); GameEndChecker.CheckCustomEndCriteria(); diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index 06b9c9a7b..433cc90bc 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -1070,7 +1070,6 @@ internal static class IntroCutsceneDestroyPatch { foreach (PlayerControl pc in aapc) { - pc.SyncSettings(); pc.RpcResetAbilityCooldown(); if (pc.GetCustomRole().UsesPetInsteadOfKill()) diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 38a8e250d..950280585 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -2130,6 +2130,7 @@ "ForensicNoticeDeathReason": "\\nThe victim's cause of death is {0}", "ForensicNoticeAddons": "\\nThe victim's add-ons are: {0}", "ForensicNoticeKillTime": "\\nThe victim died {0} seconds ago", + "ForensicNoticeColorType": "\\nThe killer has a {0} color ({1})", "SleuthMsg": "{0}'s role is: {1}", "GodNoticeAlive": "During the meeting, each player felt a revelation from heaven, and it turned out that God is still alive!", "WorkaholicAdviceAlive": "It's not recommended to kill or vote {0} out. Doing so will help them finish their tasks quicker.", @@ -3689,6 +3690,11 @@ "WhispererInfo.Time": "I died {0} seconds before the meeting started", "WhispererInfo.Role": "I was {0}", "WhispererInfo.KillerRole": "My killer was {0}", + "WhispererInfo.AloneInRoomAtDeath": "I was alone with the killer when I died", + "WhispererInfo.PlayersInSameRoomAtDeath": "{0} were in the same room as me when I died", + "WhispererInfo.NoSabotageAtDeath": "No sabotage was ongoing when I died", + "WhispererInfo.SabotageAtDeath": "I died during {0}", + "WhispererInfo.DeathReason": "My death reason is {0}", "WhispererQuestioning": "Questioning {0}... {1}%", "Leery": "Leery", "LeeryInfo": "Investigate Who Isn't a Crewmate", diff --git a/Roles/Coven/Banshee.cs b/Roles/Coven/Banshee.cs index e2794f185..995b5a7ec 100644 --- a/Roles/Coven/Banshee.cs +++ b/Roles/Coven/Banshee.cs @@ -9,7 +9,7 @@ namespace EHR.Coven; public class Banshee : Coven { public static bool On; - private static List Instances = []; + public static List Instances = []; private static OptionItem AbilityCooldown; private static OptionItem Radius; diff --git a/Roles/Crewmate/CopyCat.cs b/Roles/Crewmate/CopyCat.cs index 48c608049..5e1d423d7 100644 --- a/Roles/Crewmate/CopyCat.cs +++ b/Roles/Crewmate/CopyCat.cs @@ -115,6 +115,22 @@ public class CopyCat : RoleBase CustomRoles.Vindicator or CustomRoles.Pickpocket => CustomRoles.Mayor, CustomRoles.Councillor => CustomRoles.Judge, CustomRoles.EvilGuesser or CustomRoles.Doomsayer => CustomRoles.NiceGuesser, + CustomRoles.Vengeance => CustomRoles.Adrenaline, + CustomRoles.Occultist => CustomRoles.Altruist, + CustomRoles.Dealer => CustomRoles.Merchant, + CustomRoles.Romantic => CustomRoles.Aid, + CustomRoles.Chemist => CustomRoles.Alchemist, + CustomRoles.Bomber or CustomRoles.Nuker => CustomRoles.Tree, + CustomRoles.Siren => CustomRoles.Rhapsode, + CustomRoles.Consort => CustomRoles.Escort, + CustomRoles.Parasite => CustomRoles.DoubleAgent, + CustomRoles.BloodKnight or CustomRoles.Wildling => CustomRoles.Safeguard, + CustomRoles.PlagueBearer => CustomRoles.Socialite, + CustomRoles.Demon => CustomRoles.Spy, + CustomRoles.Kidnapper => CustomRoles.Autocrat, + CustomRoles.Capitalism => CustomRoles.Helper, + CustomRoles.Technician or CustomRoles.Saboteur => CustomRoles.Mechanic, + CustomRoles.Magician => CustomRoles.Wizard, _ => role }; } diff --git a/Roles/Crewmate/Forensic.cs b/Roles/Crewmate/Forensic.cs index fe527aa19..9d4cfc53e 100644 --- a/Roles/Crewmate/Forensic.cs +++ b/Roles/Crewmate/Forensic.cs @@ -10,6 +10,7 @@ internal class Forensic : RoleBase public static OptionItem ForensicCanknowDeathReason; public static OptionItem ForensicCanknowAddons; public static OptionItem ForensicCanknowKillTime; + public static OptionItem ForensicCanknowColorType; public static Dictionary ForensicNotify = []; @@ -30,6 +31,9 @@ internal class Forensic : RoleBase ForensicCanknowKillTime = new BooleanOptionItem(6613, "ForensicCanknowKillTime", true, TabGroup.CrewmateRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Forensic]); + + ForensicCanknowColorType = new BooleanOptionItem(6614, "ForensicCanknowColorType", false, TabGroup.CrewmateRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Forensic]); } public override void Init() { } @@ -40,10 +44,10 @@ internal class Forensic : RoleBase { string msg = string.Format(Translator.GetString("ForensicNoticeVictim"), tpc.GetRealName(), tpc.GetCustomRole().ToColoredString()); + PlayerControl realKiller = tpc.GetRealKiller(); + if (ForensicCanknowKiller.GetBool()) { - PlayerControl realKiller = tpc.GetRealKiller(); - if (realKiller == null) msg += ";" + Translator.GetString("ForensicNoticeKillerNotFound"); else @@ -62,6 +66,16 @@ internal class Forensic : RoleBase msg += ";" + string.Format(Translator.GetString("ForensicNoticeKillTime"), (int)timeSpanSeconds); } + if (ForensicCanknowColorType.GetBool() && realKiller != null) + { + var darker = new List { 0, 1, 2, 6, 8, 9, 12, 15 }; + bool isDarker = darker.Contains(realKiller.CurrentOutfit.ColorId); + Func selector = x => Utils.ColorString(Palette.PlayerColors[x], Palette.GetColorName(x)); + var colors = isDarker ? string.Join('/', darker.Select(selector)) : string.Join('/', Enumerable.Range(0, 18).Except(darker).Select(selector)); + var str = Translator.GetString(isDarker ? "WhispererInfo.ColorDark" : "WhispererInfo.ColorLight"); + msg += ";" + string.Format(Translator.GetString("ForensicNoticeColorType"), str, colors); + } + ForensicNotify[player.PlayerId] = msg; } } \ No newline at end of file diff --git a/Roles/Crewmate/Whisperer.cs b/Roles/Crewmate/Whisperer.cs index e23dcd736..6d02f87c1 100644 --- a/Roles/Crewmate/Whisperer.cs +++ b/Roles/Crewmate/Whisperer.cs @@ -19,12 +19,14 @@ public class Whisperer : RoleBase public static OptionItem AbilityChargesWhenFinishedTasks; private static DateTime LastMeetingStart; + private static Dictionary DeathInfo = []; private int Count; private (string Name, int Percent) CurrentlyQuestioning; private List Info; private List Souls; private byte WhispererId; + public override bool IsEnable => On; public override void SetupCustomOption() @@ -36,7 +38,7 @@ public class Whisperer : RoleBase .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Whisperer]) .SetValueFormat(OptionFormat.Seconds); - Duration = new IntegerOptionItem(++id, "WhispererDuration", new(0, 60, 1), 7, TabGroup.CrewmateRoles) + Duration = new IntegerOptionItem(++id, "WhispererDuration", new(0, 60, 1), 4, TabGroup.CrewmateRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Whisperer]) .SetValueFormat(OptionFormat.Seconds); @@ -44,7 +46,7 @@ public class Whisperer : RoleBase .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Whisperer]) .SetValueFormat(OptionFormat.Times); - WhispererAbilityUseGainWithEachTaskCompleted = new FloatOptionItem(++id, "AbilityUseGainWithEachTaskCompleted", new(0f, 5f, 0.05f), 1f, TabGroup.CrewmateRoles) + WhispererAbilityUseGainWithEachTaskCompleted = new FloatOptionItem(++id, "AbilityUseGainWithEachTaskCompleted", new(0f, 5f, 0.05f), 3.5f, TabGroup.CrewmateRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Whisperer]) .SetValueFormat(OptionFormat.Times); @@ -57,6 +59,7 @@ public class Whisperer : RoleBase { On = false; Instances = []; + DeathInfo = []; } public override void Add(byte playerId) @@ -100,7 +103,7 @@ public class Whisperer : RoleBase byte soulPlayerId = soul.Player.PlayerId; - if (Vector2.Distance(pc.Pos(), soul.Position) > 1f) + if (Vector2.Distance(pc.Pos(), soul.Position) > 1.5f) { soul.QuestioningTime = 0f; Utils.SendRPC(CustomRPC.SyncRoleData, WhispererId, 3, soulPlayerId, soul.QuestioningTime); @@ -124,15 +127,22 @@ public class Whisperer : RoleBase { PlayerState state = Main.PlayerStates[soulPlayerId]; (DateTime TimeStamp, byte ID) killer = state.RealKiller; - int next = IRandom.Instance.Next(4); + int next = IRandom.Instance.Next(7); if (state.deathReason == PlayerState.DeathReason.Disconnected) next = 2; + (byte[] SameRoomPlayers, SystemTypes? ActiveSabotage) deathInfo = ([], null); + if (next > 3 && !DeathInfo.TryGetValue(soulPlayerId, out deathInfo)) next = IRandom.Instance.Next(4); + if (pc.Is(CustomRoles.Autopsy) || pc.Is(CustomRoles.Doctor) || Options.EveryoneSeesDeathReasons.GetBool()) next = IRandom.Instance.Next(6); + PlayerState killerState = Main.PlayerStates[killer.ID]; info = next switch { 0 => string.Format(Translator.GetString("WhispererInfo.Color"), GetColorInfo(Utils.GetPlayerInfoById(killer.ID).DefaultOutfit.ColorId, out string colors), colors), 1 => string.Format(Translator.GetString("WhispererInfo.Time"), (int)Math.Round((LastMeetingStart - killer.TimeStamp).TotalSeconds)), - 2 => string.Format(Translator.GetString("WhispererInfo.Role"), state.MainRole.ToColoredString()), - 3 => string.Format(Translator.GetString("WhispererInfo.KillerRole"), Main.PlayerStates[killer.ID].MainRole.ToColoredString()), + 2 => string.Format(Translator.GetString("WhispererInfo.Role"), state.MainRole.ToColoredString() + (state.SubRoles.Count == 0 ? string.Empty : string.Join(' ', state.SubRoles.ConvertAll(x => x.ToColoredString())))), + 3 => string.Format(Translator.GetString("WhispererInfo.KillerRole"), killerState.MainRole.ToColoredString() + (killerState.SubRoles.Count == 0 ? string.Empty : string.Join(' ', killerState.SubRoles.ConvertAll(x => x.ToColoredString())))), + 4 => string.Format(Translator.GetString(deathInfo.SameRoomPlayers.Length == 0 ? "WhispererInfo.AloneInRoomAtDeath" : "WhispererInfo.PlayersInSameRoomAtDeath"), string.Join(", ", deathInfo.SameRoomPlayers.Select(x => x.ColoredPlayerName()))), + 5 => string.Format(Translator.GetString(deathInfo.ActiveSabotage.HasValue ? "WhispererInfo.NoSabotageAtDeath" : "WhispererInfo.SabotageAtDeath"), Translator.GetString(deathInfo.ActiveSabotage.HasValue ? deathInfo.ActiveSabotage.ToString() : "None")), + 6 => string.Format(Translator.GetString("WhispererInfo.DeathReason"), Translator.GetString($"DeathReason.{state.deathReason}")), _ => string.Empty }; @@ -188,6 +198,9 @@ public class Whisperer : RoleBase instance.Souls.Add(new(target)); Utils.SendRPC(CustomRPC.SyncRoleData, instance.WhispererId, 5, target.PlayerId); } + + var room = target.GetPlainShipRoom(); + DeathInfo[target.PlayerId] = (room == null ? [] : Main.AllAlivePlayerControls.Where(x => x.GetPlainShipRoom() == room).Select(x => x.PlayerId).ToArray(), new[] { SystemTypes.Electrical, SystemTypes.Reactor, SystemTypes.Laboratory, SystemTypes.LifeSupp, SystemTypes.Comms, SystemTypes.HeliSabotage, SystemTypes.MushroomMixupSabotage, (SystemTypes)SubmergedCompatibility.SubmergedSystemTypes.Ballast }.FindFirst(Utils.IsActive, out var sabotage) ? sabotage : null); } public override void OnReportDeadBody() @@ -233,7 +246,6 @@ public class Whisperer : RoleBase public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) { if (seer.PlayerId != target.PlayerId || seer.PlayerId != WhispererId || meeting || (seer.IsModdedClient() && !hud)) return string.Empty; - return "" + string.Join('\n', Info) + (CurrentlyQuestioning.Percent > 0 ? "\n" + string.Format(Translator.GetString("WhispererQuestioning"), CurrentlyQuestioning.Name, CurrentlyQuestioning.Percent) : string.Empty) + ""; }