diff --git a/GameMode/CaptureTheFlag.cs b/GameMode/CaptureTheFlag.cs index 30eb8feeb..e27b9a41c 100644 --- a/GameMode/CaptureTheFlag.cs +++ b/GameMode/CaptureTheFlag.cs @@ -379,7 +379,7 @@ public static class CaptureTheFlag try { var pc1 = id1.GetPlayer(); - if (pc1 == null) continue; + if (pc1 == null || pc1.AmOwner) continue; var sender = CustomRpcSender.Create("CTF Set Teams"); sender.StartMessage(pc1.OwnerId); diff --git a/Main.cs b/Main.cs index d03264461..3a61a4a5b 100644 --- a/Main.cs +++ b/Main.cs @@ -196,6 +196,7 @@ public class Main : BasePlugin public static ConfigEntry SwitchVanilla { get; private set; } public static ConfigEntry GodMode { get; private set; } public static ConfigEntry DarkTheme { get; private set; } + public static ConfigEntry DarkThemeForMeetingUI { get; private set; } public static ConfigEntry HorseMode { get; private set; } public static ConfigEntry LongMode { get; private set; } public static ConfigEntry ShowPlayerInfoInLobby { get; private set; } @@ -298,6 +299,7 @@ public class Main : BasePlugin SwitchVanilla = Config.Bind("Client Options", "SwitchVanilla", false); GodMode = Config.Bind("Client Options", "GodMode", false); DarkTheme = Config.Bind("Client Options", "DarkTheme", true); + DarkThemeForMeetingUI = Config.Bind("Client Options", "DarkThemeForMeetingUI", false); HorseMode = Config.Bind("Client Options", "HorseMode", false); LongMode = Config.Bind("Client Options", "LongMode", false); ShowPlayerInfoInLobby = Config.Bind("Client Options", "ShowPlayerInfoInLobby", false); diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 9cfa18938..61586a408 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -950,7 +950,7 @@ internal static class CustomRolesHelper public static bool NeedUpdateOnLights(this CustomRoles role) { - return !role.UsesPetInsteadOfKill() && role is CustomRoles.Lighter or CustomRoles.Wiper; + return role is CustomRoles.Wiper; } public static bool IsBetrayalAddon(this CustomRoles role) @@ -1213,7 +1213,7 @@ internal static class CustomRolesHelper 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, CustomRoles.Magnet when pc.GetRoleTypes() is not (RoleTypes.Impostor or RoleTypes.Phantom or RoleTypes.Shapeshifter or RoleTypes.Viper) => false, - CustomRoles.Haste when pc.GetRoleTypes() is not (RoleTypes.Impostor or RoleTypes.Phantom or RoleTypes.Shapeshifter or RoleTypes.Viper) || !pc.CanUseImpostorVentButton() => false, + CustomRoles.Haste when pc.GetRoleTypes() is not (RoleTypes.Impostor or RoleTypes.Phantom or RoleTypes.Shapeshifter or RoleTypes.Viper) || !pc.CanUseImpostorVentButton() || pc.Is(CustomRoles.Glitch) => false, CustomRoles.Diseased when pc.Is(CustomRoles.Antidote) => false, CustomRoles.Antidote when pc.Is(CustomRoles.Diseased) => false, CustomRoles.Flashman or CustomRoles.Giant when pc.GetCustomRole() is CustomRoles.Swooper or CustomRoles.Wraith or CustomRoles.Chameleon or CustomRoles.Alchemist or CustomRoles.Ankylosaurus => false, diff --git a/Modules/CustomTeamManager.cs b/Modules/CustomTeamManager.cs index 06115df02..c6f142e5b 100644 --- a/Modules/CustomTeamManager.cs +++ b/Modules/CustomTeamManager.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using AmongUs.GameOptions; +using Hazel; using UnityEngine; namespace EHR.Modules; @@ -78,8 +80,10 @@ internal static class CustomTeamManager UpdateEnabledTeams(); if (EnabledCustomTeams.Count == 0) return; + PlayerControl[] aapc = Main.AllAlivePlayerControls; + CustomTeamPlayerIds = Main.PlayerStates - .IntersectBy(Main.AllAlivePlayerControls.Select(x => x.PlayerId), x => x.Key) + .IntersectBy(aapc.Select(x => x.PlayerId), x => x.Key) .GroupBy(x => EnabledCustomTeams.FirstOrDefault(t => t.TeamMembers.Contains(x.Value.MainRole)), x => x.Key) .Where(x => x.Key != null) .ToDictionary(x => x.Key, x => TakeAsManyAsSet(x, x.Key)); @@ -107,6 +111,19 @@ internal static class CustomTeamManager } } + var imps = aapc.Where(x => x.IsImpostor() && !CustomTeamPlayerIds.Values.Any(l => l.Contains(x.PlayerId))).ToList(); + + if (imps.Count > 0) + { + CustomTeamPlayerIds.Values.Flatten().ToValidPlayers().DoIf(x => x != null && x.IsImpostor() && !IsSettingEnabledForPlayerTeam(x.PlayerId, CTAOption.WinWithOriginalTeam), ctp => + { + imps.ForEach(imp => ctp.RpcSetRoleDesync(RoleTypes.Crewmate, imp.OwnerId, setRoleMap: true)); + var sender = CustomRpcSender.Create("CustomTeamManager_SetImpostorDesync", SendOption.Reliable); + imps.ForEach(imp => sender.RpcSetRole(imp, RoleTypes.Crewmate, ctp.OwnerId, changeRoleMap: true)); + sender.SendMessage(); + }); + } + return; static HashSet TakeAsManyAsSet(IEnumerable playerIds, CustomTeam customTeam) diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 44349ecc3..8a0086c43 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -377,7 +377,7 @@ internal static class ExtendedPlayerControl } // If you use vanilla RpcSetRole, it will block further SetRole calls until the next game starts. - public static void RpcSetRoleGlobal(this PlayerControl player, RoleTypes roleTypes) + public static void RpcSetRoleGlobal(this PlayerControl player, RoleTypes roleTypes, bool setRoleMap = false) { if (!AmongUsClient.Instance.AmHost) return; if (AmongUsClient.Instance.AmClient) player.StartCoroutine(player.CoSetRole(roleTypes, true)); @@ -386,6 +386,19 @@ internal static class ExtendedPlayerControl writer.Write(true); AmongUsClient.Instance.FinishRpcImmediately(writer); Logger.Info($" {player.GetNameWithRole()} => {roleTypes}", "RpcSetRoleGlobal"); + + if (setRoleMap) + { + foreach ((byte seerID, byte targetID) in StartGameHostPatch.RpcSetRoleReplacer.RoleMap.Keys.ToArray()) + { + if (targetID == player.PlayerId) + { + var value = StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(seerID, targetID)]; + value.RoleType = roleTypes; + StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(seerID, targetID)] = value; + } + } + } } public static void RpcSetRoleDesync(this PlayerControl player, RoleTypes role, int clientId, bool setRoleMap = false) diff --git a/Modules/FixedUpdateCaller.cs b/Modules/FixedUpdateCaller.cs index 0abea685d..b8cf6b244 100644 --- a/Modules/FixedUpdateCaller.cs +++ b/Modules/FixedUpdateCaller.cs @@ -127,6 +127,13 @@ public static class FixedUpdateCaller } catch { } + try + { + if (amongUsClient.AmHost && GameStates.InGame && !GameStates.IsEnded) + FixedUpdatePatch.LoversSuicide(); + } + catch (Exception e) { Utils.ThrowException(e); } + bool lobby = GameStates.IsLobby; if (lobby || (Main.IntroDestroyed && GameStates.InGame && !GameStates.IsMeeting && !ExileController.Instance && !AntiBlackout.SkipTasks)) diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index 700f41657..6047459f8 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -59,7 +59,7 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS { GameOptionsSender allSender = AllSenders[index]; - if (allSender is PlayerGameOptionsSender { IsDirty: false } sender && sender.player.IsAlive() && (sender.player.HasDesyncRole() || sender.player.GetCustomRole() == CustomRoles.Transporter || sender.player.Is(CustomRoles.Torch) || sender.player.Is(CustomRoles.Mare) || sender.player.Is(CustomRoles.Sleep) || Beacon.IsAffectedPlayer(sender.player.PlayerId))) + if (allSender is PlayerGameOptionsSender { IsDirty: false } sender && sender.player.IsAlive() && (sender.player.HasDesyncRole() || sender.player.GetCustomRole() is CustomRoles.Transporter or CustomRoles.Lighter || sender.player.Is(CustomRoles.Torch) || sender.player.Is(CustomRoles.Mare) || sender.player.Is(CustomRoles.Sleep) || Beacon.IsAffectedPlayer(sender.player.PlayerId))) sender.SetDirty(); } } diff --git a/Modules/GameState.cs b/Modules/GameState.cs index 69eb21b19..2bc43aee4 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -114,7 +114,7 @@ public class PlayerState(byte playerId) public void SetMainRole(CustomRoles role) { - try { Utils.RemovePlayerFromPreviousRoleData(Player); } + try { Role.Remove(PlayerId); } catch (Exception e) { Utils.ThrowException(e); } if (Main.IntroDestroyed && (RoleHistory.Count == 0 || RoleHistory[^1] != MainRole)) diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs index 5d729949a..e3e0e6bd8 100644 --- a/Modules/GuessManager.cs +++ b/Modules/GuessManager.cs @@ -999,7 +999,7 @@ public static class GuessManager if (selectedButton != button) { selectedButton = button; - buttons.ForEach(x => x.GetComponent().color = x == selectedButton ? Utils.GetRoleColor(PlayerControl.LocalPlayer.GetCustomRole()) : Main.DarkTheme.Value ? new Color(0.1f, 0.1f, 0.1f) : Color.white); + buttons.ForEach(x => x.GetComponent().color = x == selectedButton ? Utils.GetRoleColor(PlayerControl.LocalPlayer.GetCustomRole()) : Main.DarkThemeForMeetingUI.Value ? new Color(0.1f, 0.1f, 0.1f) : Color.white); } else { diff --git a/Modules/LobbySharingAPI.cs b/Modules/LobbySharingAPI.cs index 84139620d..824620303 100644 --- a/Modules/LobbySharingAPI.cs +++ b/Modules/LobbySharingAPI.cs @@ -146,11 +146,13 @@ public enum LobbyStatus Closed } -[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.ExitGame))] +[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.DisconnectInternal))] internal static class ExitGamePatch { - public static void Prefix() + public static void Prefix(InnerNetClient __instance) { + if (__instance is not AmongUsClient) return; + Logger.Msg("Exiting game", "ExitGamePatch.Prefix"); Main.RealOptionsData?.Restore(GameOptionsManager.Instance.CurrentGameOptions); @@ -162,8 +164,10 @@ internal static class ExitGamePatch } } - public static void Postfix() + public static void Postfix(InnerNetClient __instance) { + if (__instance is not AmongUsClient) return; + LobbySharingAPI.NotifyLobbyStatusChanged(LobbyStatus.Closed); GameEndChecker.LoadingEndScreen = false; diff --git a/Modules/RPC.cs b/Modules/RPC.cs index 8d62fdec5..201321d54 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -1481,7 +1481,6 @@ internal static class RPC int amountAllOptions = OptionItem.AllOptions.Count; - Logger.Msg(" Starting", "SyncCustomSettings"); MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncCustomSettings, SendOption.Reliable, targetId); writer.Write(startAmount); writer.Write(lastAmount); @@ -1492,13 +1491,12 @@ internal static class RPC for (int option = startAmount; option < amountAllOptions && option <= lastAmount; option++) listOptions.Add(OptionItem.AllOptions[option]); int countListOptions = listOptions.Count; - Logger.Msg($"StartAmount: {startAmount} - LastAmount: {lastAmount} ({startAmount}/{lastAmount}) :--: ListOptionsCount: {countListOptions} - AllOptions: {amountAllOptions} ({countListOptions}/{amountAllOptions})", "SyncCustomSettings"); + Logger.Info($"StartAmount: {startAmount} - LastAmount: {lastAmount} ({startAmount}/{lastAmount}) :--: ListOptionsCount: {countListOptions} - AllOptions: {amountAllOptions} ({countListOptions}/{amountAllOptions})", "SyncCustomSettings"); // Sync Settings foreach (OptionItem option in listOptions) writer.WritePacked(option.GetValue()); AmongUsClient.Instance.FinishRpcImmediately(writer); - Logger.Msg(" Finished", "SyncCustomSettings"); } public static void PlaySoundRPC(byte playerID, Sounds sound) diff --git a/Modules/Translator.cs b/Modules/Translator.cs index d6dba7713..2bb5ff90c 100644 --- a/Modules/Translator.cs +++ b/Modules/Translator.cs @@ -165,7 +165,7 @@ public static class Translator try { - if (TranslateMaps.TryGetValue(str, out Dictionary dic) && (!dic.TryGetValue((int)langId, out res) || string.IsNullOrWhiteSpace(res) || (langId is not SupportedLangs.SChinese and not SupportedLangs.TChinese && Regex.IsMatch(res, @"[\u4e00-\u9fa5]") && res == GetString(str, SupportedLangs.SChinese)))) + if (TranslateMaps.TryGetValue(str, out Dictionary dic) && (!dic.TryGetValue((int)langId, out res) || string.IsNullOrEmpty(res) || (langId is not SupportedLangs.SChinese and not SupportedLangs.TChinese && Regex.IsMatch(res, @"[\u4e00-\u9fa5]") && res == GetString(str, SupportedLangs.SChinese)))) res = langId == SupportedLangs.English ? $"*{str}" : GetString(str, SupportedLangs.English); if (!TranslateMaps.ContainsKey(str) && Enum.GetValues().FindFirst(x => x.ToString() == str, out StringNames stringNames)) diff --git a/Modules/Utils.cs b/Modules/Utils.cs index c6431850f..b8b575101 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -460,12 +460,6 @@ public static class Utils } } - public static void RemovePlayerFromPreviousRoleData(PlayerControl target) - { - if (!Main.PlayerStates.TryGetValue(target.PlayerId, out PlayerState state)) return; - state.Role.Remove(target.PlayerId); - } - public static string GetDisplayRoleName(byte playerId, bool pure = false, bool seeTargetBetrayalAddons = false) { (string, Color) textData = GetRoleText(playerId, playerId, pure, seeTargetBetrayalAddons); @@ -1177,7 +1171,7 @@ public static class Utils } TaskState taskState = state.TaskState; - if (!taskState.HasTasks) return string.Empty; + if (!taskState.HasTasks || taskState.AllTasksCount == 0) return string.Empty; NetworkedPlayerInfo info = GetPlayerInfoById(playerId); bool hasTasks = HasTasks(info); @@ -3455,10 +3449,6 @@ public static class Utils Main.AbilityCD[playerId] = (TimeStamp, cd); SendRPC(CustomRPC.SyncAbilityCD, 1, playerId, cd); - - if (role.SimpleAbilityTrigger() && - (Options.UsePhantomBasis.GetBool() && (!role.IsNeutral() || Options.UsePhantomBasisForNKs.GetBool())) && !role.AlwaysUsesPhantomBase()) - GetPlayerById(playerId)?.RpcResetAbilityCooldown(); } public static (RoleTypes RoleType, CustomRoles CustomRole) GetRoleMap(byte seerId, byte targetId = byte.MaxValue) diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index 26e8006cb..005cb2ad9 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -291,17 +291,17 @@ internal static class GameEndChecker if (WinnerTeam != CustomWinner.CustomTeam && CustomTeamManager.EnabledCustomTeams.Count > 0) { - Dictionary> teams = Main.AllPlayerControls + Dictionary teams = Main.AllPlayerControls .Select(x => new { Team = CustomTeamManager.GetCustomTeam(x.PlayerId), Player = x }) .Where(x => x.Team != null) .GroupBy(x => x.Team) - .ToDictionary(x => x.Key, x => x.Select(y => y.Player.PlayerId)); + .ToDictionary(x => x.Key, x => x.Select(y => y.Player.PlayerId).ToArray()); - foreach ((CustomTeamManager.CustomTeam team, IEnumerable playerIds) in teams) + foreach ((CustomTeamManager.CustomTeam team, byte[] playerIds) in teams) { bool winWithOriginalTeam = CustomTeamManager.IsSettingEnabledForTeam(team, CTAOption.WinWithOriginalTeam); - if (CustomTeamManager.IsSettingEnabledForTeam(team, CTAOption.OriginalWinCondition) && (winWithOriginalTeam || WinnerTeam is not (CustomWinner.Impostor or CustomWinner.Crewmate or CustomWinner.Coven or CustomWinner.Neutrals))) + if (CustomTeamManager.IsSettingEnabledForTeam(team, CTAOption.OriginalWinCondition) && playerIds.Any(WinnerIds.Contains) && (winWithOriginalTeam || WinnerTeam is not (CustomWinner.Impostor or CustomWinner.Crewmate or CustomWinner.Coven or CustomWinner.Neutrals))) WinnerIds.UnionWith(playerIds); else if (!winWithOriginalTeam) WinnerIds.ExceptWith(playerIds); diff --git a/Patches/ClientOptionsPatch.cs b/Patches/ClientOptionsPatch.cs index 2c370b2de..db575df6f 100644 --- a/Patches/ClientOptionsPatch.cs +++ b/Patches/ClientOptionsPatch.cs @@ -21,6 +21,7 @@ public static class OptionsMenuBehaviourStartPatch private static ClientOptionItem EnableCustomSoundEffect; private static ClientOptionItem SwitchVanilla; private static ClientOptionItem DarkTheme; + private static ClientOptionItem DarkThemeForMeetingUI; private static ClientOptionItem HorseMode; private static ClientOptionItem LongMode; private static ClientOptionItem ShowPlayerInfoInLobby; @@ -130,6 +131,9 @@ public static class OptionsMenuBehaviourStartPatch if (DarkTheme == null || DarkTheme.ToggleButton == null) DarkTheme = ClientOptionItem.Create("EnableDarkTheme", Main.DarkTheme, __instance); + + if (DarkThemeForMeetingUI == null || DarkThemeForMeetingUI.ToggleButton == null) + DarkThemeForMeetingUI = ClientOptionItem.Create("DarkThemeForMeetingUI", Main.DarkThemeForMeetingUI, __instance); if (HorseMode == null || HorseMode.ToggleButton == null) { diff --git a/Patches/ExilePatch.cs b/Patches/ExilePatch.cs index 02c412363..b645eff4e 100644 --- a/Patches/ExilePatch.cs +++ b/Patches/ExilePatch.cs @@ -89,7 +89,7 @@ internal static class ExileControllerWrapUpPatch } pc.ResetKillCooldown(false); - pc.RpcResetAbilityCooldown(); + if (!Utils.ShouldNotApplyAbilityCooldownAfterMeeting(pc)) pc.RpcResetAbilityCooldown(); PetsHelper.RpcRemovePet(pc); } diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index 6f9b01b44..a5aeb2938 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -1418,7 +1418,7 @@ static class NamePlateDarkThemePatch { public static void Postfix(PlayerVoteArea __instance) { - if (Main.DarkTheme.Value) + if (Main.DarkThemeForMeetingUI.Value) __instance.Background.color = new Color(0.1f, 0.1f, 0.1f); } } @@ -1428,7 +1428,7 @@ static class MeetingHud_Start { public static void Postfix(MeetingHud __instance) { - if (!Main.DarkTheme.Value) return; + if (!Main.DarkThemeForMeetingUI.Value) return; __instance.meetingContents.transform.FindChild("PhoneUI").FindChild("baseColor").GetComponent().color = new Color(0.01f, 0.01f, 0.01f); __instance.Glass.color = new Color(0.7f, 0.7f, 0.7f, 0.3f); __instance.SkipVoteButton.GetComponent().color = new Color(0.4f, 0.4f, 0.4f); diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index a4f2afd4c..bc08f13c6 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -869,26 +869,6 @@ internal static class StartGameHostPatch } catch (Exception e) { Utils.ThrowException(e); } - try - { - if (CustomTeamManager.EnabledCustomTeams.Count > 0) - { - var imps = RoleResult.Where(x => x.Value.IsImpostor() && !CustomTeamManager.CustomTeamPlayerIds.Values.Any(l => l.Contains(x.Key))).Select(x => x.Key).ToValidPlayers().ToHashSet(); - - if (imps.Count > 0) - { - CustomTeamManager.CustomTeamPlayerIds.Values.Flatten().ToValidPlayers().DoIf(x => x != null && x.IsImpostor() && !CustomTeamManager.IsSettingEnabledForPlayerTeam(x.PlayerId, CTAOption.WinWithOriginalTeam), ctp => - { - imps.Do(imp => ctp.RpcSetRoleDesync(RoleTypes.Scientist, imp.OwnerId, setRoleMap: true)); - var sender = CustomRpcSender.Create("CustomTeamManager_SetImpostorDesync", SendOption.Reliable); - imps.Do(imp => sender.RpcSetRole(imp, RoleTypes.Scientist, ctp.OwnerId, changeRoleMap: true)); - sender.SendMessage(); - }); - } - } - } - catch (Exception e) { Utils.ThrowException(e); } - try { foreach (PlayerControl pc in Main.AllPlayerControls) @@ -1043,7 +1023,7 @@ internal static class StartGameHostPatch } catch (Exception e) { Utils.ThrowException(e); } - LateTask.New(CustomTeamManager.InitializeCustomTeamPlayers, 7f, log: false); + LateTask.New(CustomTeamManager.InitializeCustomTeamPlayers, 4f, log: false); if (overrideLovers) Logger.Msg(Main.LoversPlayers.Join(x => x?.GetRealName()), "Lovers"); diff --git a/Patches/PhantomRolePatch.cs b/Patches/PhantomRolePatch.cs index 7e9d82f72..e2260154d 100644 --- a/Patches/PhantomRolePatch.cs +++ b/Patches/PhantomRolePatch.cs @@ -66,7 +66,7 @@ public static class PhantomRolePatch { HudManager.Instance.AbilityButton.SetFromSettings(phantom.Data.Role.Ability); if (Utils.ShouldNotApplyAbilityCooldown(roleBase)) return false; - phantom.Data.Role.SetCooldown(); + phantom.RpcResetAbilityCooldown(); return false; } @@ -78,17 +78,13 @@ public static class PhantomRolePatch .Write(true) .EndRpc(); - if (Utils.ShouldNotApplyAbilityCooldown(roleBase)) + if (!Utils.ShouldNotApplyAbilityCooldown(roleBase)) { - sender.EndMessage(); - sender.SendMessage(); - return false; + sender.StartRpc(phantom.NetId, RpcCalls.ProtectPlayer) + .WriteNetObject(phantom) + .Write(0) + .EndRpc(); } - - sender.StartRpc(phantom.NetId, RpcCalls.ProtectPlayer) - .WriteNetObject(phantom) - .Write(0) - .EndRpc(); sender.EndMessage(); sender.SendMessage(); diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index bd49b90ea..f2e9362ff 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -648,7 +648,7 @@ internal static class MurderPlayerPatch if (target.AmOwner) RemoveDisableDevicesPatch.UpdateDisableDevices(); if (!target.Data.IsDead || !AmongUsClient.Instance.AmHost) return; - if (Butcher.ButcherDeadPlayerList.Contains(target.PlayerId)) return; + if (Butcher.ButcherDeadPlayerList.Contains(target.PlayerId) || (__instance.AmOwner && __instance.Is(CustomRoles.SoulCollector))) return; PlayerControl killer = __instance; // Alternative variable @@ -1377,7 +1377,8 @@ internal static class ReportDeadBodyPatch RpcChangeSkin(pc, new NetworkedPlayerInfo.PlayerOutfit().Set(name, 15, "", "", "", "", "")); } - if (pc.IsShifted()) pc.RpcShapeshift(pc, false); + if (Main.CheckShapeshift.ContainsKey(pc.PlayerId)) + pc.RpcShapeshift(pc, false); } else if (!pc.Data.IsDead) pc.RpcExileV2(); @@ -1671,9 +1672,6 @@ internal static class FixedUpdatePatch if (AmongUsClient.Instance.AmHost && inTask && alive && Options.LadderDeath.GetBool()) FallFromLadder.FixedUpdate(player); - if (self && GameStates.IsInGame) - LoversSuicide(); - if (inTask && self && Options.DisableDevices.GetBool()) DisableDevice.FixedUpdate(); diff --git a/Patches/SabotageSystemPatch.cs b/Patches/SabotageSystemPatch.cs index ef804d668..c9348ac71 100644 --- a/Patches/SabotageSystemPatch.cs +++ b/Patches/SabotageSystemPatch.cs @@ -181,7 +181,7 @@ public static class MushroomMixupSabotageSystemPatch LateTask.New(() => { // After MushroomMixup sabotage, shapeshift cooldown sets to 0 - Main.AllAlivePlayerControls.DoIf(x => x.GetRoleTypes() != RoleTypes.Engineer, x => x.RpcResetAbilityCooldown()); + Main.AllAlivePlayerControls.DoIf(x => x.GetRoleTypes() == RoleTypes.Shapeshifter, x => x.RpcResetAbilityCooldown()); ReportDeadBodyPatch.CanReport.SetAllValues(true); }, 1.2f, "Reset Ability Cooldown Arter Mushroom Mixup"); diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index d55f52f02..3a7e755b8 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -1064,7 +1064,8 @@ "ForceOwnLanguage": "Force mod to use your language if possible", "ForceOwnLanguageRoleName": "Force role names in your language if possible", "GodMode": "God Mode", - "EnableDarkTheme": "Enable Dark Theme", + "EnableDarkTheme": "Dark Theme for Chat UI", + "DarkThemeForMeetingUI": "Dark Theme for Meeting UI", "AutoDisplayKillLog": "Display Kill-log", "AutoDisplayLastRoles": "Display Last Roles", "AutoDisplayLastAddOns": "Display Last Add-ons", @@ -1332,6 +1333,7 @@ "CanKill": "Can Kill", "CanShapeshift": "Can Shapeshift", "CanGuess": "Can Guess in Guesser Mode", + "CooldownsResetEachOther": "Ability Cooldown and Kill Cooldown Reset Each Other", "BombCooldown": "Bomb Cooldown", "AbilityUseGainWithEachTaskCompleted": "Ability Use Gains With Each Task Completed", "AbilityUseGainWithEachKill": "Ability Use Gains With Each Kill", @@ -9896,6 +9898,7 @@ "ThiefInfo": "Steal a Killing Role", "ThiefInfoLong": "(Neutrals):\\nAs the Thief, you must kill someone to become their role:\\n- Impostor / Neutral Killing roles => become the same role as them\\n- Trickster => become Double Agent\\n- Maverick => become Serial Killer\\n- Madmate roles => become Renegade\\n- Coven roles (if enabled) => become Coven Member\\nIf you kill any other role, you suicide.", "Thief.CanStealCovenRoles": "Can Steal <#7b3fbb>Coven Roles", + "Thief.CanKillRound1": "Can Kill on the First Round", "Sensor": "Sensor", "SensorInfo": "Whenever Someone Sabotages, You See an Arrow Towards Them", "SensorInfoLong": "(Crewmates):\\nAs the Sensor, whenever someone sabotages, you get a temporary arrow pointing at them.", diff --git a/Roles/Crewmate/Dad.cs b/Roles/Crewmate/Dad.cs index 206437a4e..db3476d4f 100644 --- a/Roles/Crewmate/Dad.cs +++ b/Roles/Crewmate/Dad.cs @@ -164,6 +164,9 @@ public class Dad : RoleBase public override void Remove(byte playerId) { Instances.Remove(this); + if (!AmongUsClient.Instance.AmHost) return; + Main.AllPlayerSpeed[playerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); + PlayerGameOptionsSender.SetDirty(playerId); } public override void ApplyGameOptions(IGameOptions opt, byte playerId) diff --git a/Roles/Crewmate/Imitator.cs b/Roles/Crewmate/Imitator.cs index ea1ef5c43..085ccde86 100644 --- a/Roles/Crewmate/Imitator.cs +++ b/Roles/Crewmate/Imitator.cs @@ -76,15 +76,18 @@ public class Imitator : RoleBase { var command = $"/imitate {playerId}"; ChatCommands.ImitateCommand(PlayerControl.LocalPlayer, command, command.Split(' ')); + + if (ImitatingRole.ContainsKey(PlayerControl.LocalPlayer.PlayerId)) + { + foreach (PlayerVoteArea pva in MeetingHud.Instance.playerStates) + { + Transform button = pva.transform.FindChild("ImitatorButton"); + if (button != null) Object.Destroy(button.gameObject); + } + } } else SendRPC(playerId); - - foreach (PlayerVoteArea pva in MeetingHud.Instance.playerStates) - { - Transform button = pva.transform.FindChild("ImitatorButton"); - if (button != null) Object.Destroy(button.gameObject); - } } private static void CreateImitatorButton(MeetingHud __instance) diff --git a/Roles/Crewmate/Inspector.cs b/Roles/Crewmate/Inspector.cs index 84d1db2eb..df3c60523 100644 --- a/Roles/Crewmate/Inspector.cs +++ b/Roles/Crewmate/Inspector.cs @@ -439,7 +439,7 @@ public class Inspector : RoleBase if (FirstPick.TryGetValue(lpcId, out byte firstPick)) { - InspectorCheckMsg(PlayerControl.LocalPlayer, $"/cp {playerId} {firstPick}"); + InspectorCheckMsg(lpcId.GetPlayer(), $"/cp {playerId} {firstPick}", lpcId.IsPlayerModdedClient()); FirstPick.Remove(lpcId); } else diff --git a/Roles/Impostor/Sapper.cs b/Roles/Impostor/Sapper.cs index 87594e823..0d382f7b7 100644 --- a/Roles/Impostor/Sapper.cs +++ b/Roles/Impostor/Sapper.cs @@ -18,6 +18,8 @@ public class Sapper : RoleBase private static OptionItem Delay; private static OptionItem Radius; private static OptionItem CanSabotage; + private static OptionItem CanKill; + private static OptionItem CooldownsResetEachOther; private static Dictionary Bombs = []; @@ -43,6 +45,12 @@ public class Sapper : RoleBase CanSabotage = new BooleanOptionItem(Id + 14, "CanSabotage", false, TabGroup.ImpostorRoles) .SetParent(CustomRoleSpawnChances[CustomRoles.Sapper]); + + CanKill = new BooleanOptionItem(Id + 15, "CanKill", true, TabGroup.ImpostorRoles) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sapper]); + + CooldownsResetEachOther = new BooleanOptionItem(Id + 16, "CooldownsResetEachOther", true, TabGroup.ImpostorRoles) + .SetParent(CanKill); } public override void Init() @@ -90,7 +98,17 @@ public class Sapper : RoleBase public override bool CanUseKillButton(PlayerControl pc) { - return false; + return CanKill.GetBool(); + } + + public override void OnMurder(PlayerControl killer, PlayerControl target) + { + if (!CooldownsResetEachOther.GetBool()) return; + + if (UsePets.GetBool() && !UsePhantomBasis.GetBool()) + killer.AddAbilityCD(); + else + killer.RpcResetAbilityCooldown(); } public override void OnPet(PlayerControl pc) @@ -108,6 +126,7 @@ public class Sapper : RoleBase if (pc == null) return false; if (!pc.IsAlive() || Pelican.IsEaten(pc.PlayerId)) return false; Bombs.TryAdd(pc.Pos(), TimeStamp); + if (CanKill.GetBool() && CooldownsResetEachOther.GetBool()) pc.SetKillCooldown(); return false; } diff --git a/Roles/Impostor/Silencer.cs b/Roles/Impostor/Silencer.cs index 08836d85a..c5033cdea 100644 --- a/Roles/Impostor/Silencer.cs +++ b/Roles/Impostor/Silencer.cs @@ -34,7 +34,7 @@ public class Silencer : RoleBase public override void SetupCustomOption() { - SetupRoleOptions(Id, TabGroup.ImpostorRoles, CustomRoles.Silencer); + SetupRoleOptions(Id, TabGroup.ImpostorRoles, CustomRoles.Silencer, zeroOne: true); SkillCooldown = new FloatOptionItem(Id + 5, "AbilityCooldown", new(2.5f, 60f, 0.5f), 30f, TabGroup.ImpostorRoles) .SetParent(CustomRoleSpawnChances[CustomRoles.Silencer]) @@ -83,12 +83,13 @@ public class Silencer : RoleBase public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) { - if (SilenceMode.GetValue() >= 1 || ForSilencer.Count >= 1) return true; + if (SilenceMode.GetValue() >= 1) return true; return killer.CheckDoubleTrigger(target, () => { - ForSilencer.Add(target.PlayerId); + ForSilencer = [target.PlayerId]; Utils.SendRPC(CustomRPC.SyncRoleData, killer.PlayerId, 1, target.PlayerId); + Utils.NotifyRoles(SpecifySeer: killer, SpecifyTarget: target); killer.SetKillCooldown(3f); if (killer.AmOwner) @@ -104,9 +105,9 @@ public class Silencer : RoleBase public override bool OnShapeshift(PlayerControl shapeshifter, PlayerControl target, bool shapeshifting) { - if (SilenceMode.GetValue() == 1 && ForSilencer.Count == 0 && shapeshifter.PlayerId != target.PlayerId) + if (SilenceMode.GetValue() == 1&& shapeshifter.PlayerId != target.PlayerId) { - ForSilencer.Add(target.PlayerId); + ForSilencer = [target.PlayerId]; Utils.SendRPC(CustomRPC.SyncRoleData, shapeshifter.PlayerId, 1, target.PlayerId); if (shapeshifter.AmOwner) @@ -124,7 +125,7 @@ public class Silencer : RoleBase public override bool OnVanish(PlayerControl pc) { - if (SilenceMode.GetValue() == 2 && ForSilencer.Count == 0) + if (SilenceMode.GetValue() == 2) { var pos = pc.Pos(); var killRange = NormalGameOptionsV10.KillDistances[Mathf.Clamp(Main.NormalOptions.KillDistance, 0, 2)]; @@ -132,8 +133,9 @@ public class Silencer : RoleBase PlayerControl target = nearPlayers.Length == 0 ? null : nearPlayers.MinBy(x => x.distance).pc; if (target == null) return false; - ForSilencer.Add(target.PlayerId); + ForSilencer = [target.PlayerId]; Utils.SendRPC(CustomRPC.SyncRoleData, pc.PlayerId, 1, target.PlayerId); + Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: target); if (pc.AmOwner) { @@ -153,7 +155,7 @@ public class Silencer : RoleBase switch (reader.ReadPackedInt32()) { case 1: - ForSilencer.Add(reader.ReadByte()); + ForSilencer = [reader.ReadByte()]; break; case 2: ForSilencer = []; @@ -163,8 +165,22 @@ public class Silencer : RoleBase public override void AfterMeetingTasks() { + if (ForSilencer.Count == 0) return; ForSilencer.Clear(); - if (PlayerIdList.Count == 0) return; - Utils.SendRPC(CustomRPC.SyncRoleData, PlayerIdList[0], 2); + PlayerIdList.ForEach(x => Utils.SendRPC(CustomRPC.SyncRoleData, x, 2)); + } + + public override void OnReportDeadBody() + { + if (ForSilencer.Count == 0) return; + Main.Instance.StartCoroutine(WaitForVote()); + return; + + System.Collections.IEnumerator WaitForVote() + { + while (MeetingHud.Instance && MeetingHud.Instance.state is not (MeetingHud.VoteStates.NotVoted or MeetingHud.VoteStates.Voted) && !GameStates.IsEnded && GameStates.InGame && !ExileController.Instance) yield return null; + if (!MeetingHud.Instance || GameStates.IsEnded || !GameStates.InGame || ExileController.Instance) yield break; + ForSilencer.ForEach(x => MeetingHud.Instance.CastVote(x, 253)); // 253 is skip vote + } } } \ No newline at end of file diff --git a/Roles/Impostor/Venerer.cs b/Roles/Impostor/Venerer.cs index 0fb414bbf..e0d62e117 100644 --- a/Roles/Impostor/Venerer.cs +++ b/Roles/Impostor/Venerer.cs @@ -97,7 +97,7 @@ public class Venerer : RoleBase Utils.NotifyRoles(SpecifyTarget: pc); LateTask.New(() => { - if (!ChangedSkin || pc == null || !pc.IsAlive()) return; + if (!ChangedSkin || pc == null || !pc.IsAlive() || Camouflage.IsCamouflage) return; Utils.RpcChangeSkin(pc, outfit); ChangedSkin = false; Utils.NotifyRoles(SpecifyTarget: pc); diff --git a/Roles/Neutral/Arsonist.cs b/Roles/Neutral/Arsonist.cs index 5830ed2dc..3abd542b4 100644 --- a/Roles/Neutral/Arsonist.cs +++ b/Roles/Neutral/Arsonist.cs @@ -95,14 +95,17 @@ internal class Arsonist : RoleBase public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) { + float cooldown = ArsonistCooldown.GetFloat(); + if (ArsonistCanIgniteAnytime.GetBool() && IsDoused[(killer.PlayerId, target.PlayerId)] && Utils.GetDousedPlayerCount(killer.PlayerId).Doused >= ArsonistMinPlayersToIgnite.GetInt()) { + killer.SetKillCooldown(cooldown); Ignite(killer.MyPhysics); return false; } float douseTime = ArsonistDouseTime.GetFloat(); - killer.SetKillCooldown(Mathf.Approximately(douseTime, 0f) ? ArsonistCooldown.GetFloat() : douseTime); + killer.SetKillCooldown(Mathf.Approximately(douseTime, 0f) ? cooldown : douseTime); if (!IsDoused[(killer.PlayerId, target.PlayerId)] && !ArsonistTimer.ContainsKey(killer.PlayerId) && killer.RpcCheckAndMurder(target, check: true)) { diff --git a/Roles/Neutral/Duality.cs b/Roles/Neutral/Duality.cs index b7a735e05..70658055a 100644 --- a/Roles/Neutral/Duality.cs +++ b/Roles/Neutral/Duality.cs @@ -1,4 +1,5 @@ -using AmongUs.GameOptions; +using System.Diagnostics; +using AmongUs.GameOptions; using EHR.Modules; using Hazel; @@ -15,7 +16,7 @@ public class Duality : RoleBase public override bool IsEnable => On; public bool KillingPhase; - private long TimerEndTS; + private Stopwatch Timer; private long LastUpdateTS; private byte DualityId; @@ -37,7 +38,8 @@ public class Duality : RoleBase On = true; KillingPhase = true; DualityId = playerId; - ResetTimer(); + Timer = new(); + LateTask.New(() => Timer = Stopwatch.StartNew(), 10f, log: false); } public override bool CanUseKillButton(PlayerControl pc) @@ -71,7 +73,7 @@ public class Duality : RoleBase LateTask.New(() => { KillingPhase = false; - killer.RpcChangeRoleBasis(CanVent.GetBool() ? CustomRoles.EngineerEHR : CustomRoles.CrewmateEHR); + killer.RpcSetRoleGlobal(CanVent.GetBool() ? RoleTypes.Engineer : RoleTypes.Crewmate); killer.RpcResetTasks(); ResetTimer(); }, 0.2f, log: false); @@ -92,7 +94,7 @@ public class Duality : RoleBase if (LastUpdateTS == now) return; LastUpdateTS = now; - if (now >= TimerEndTS) + if (GetRemainingTime() <= 0) { pc.Suicide(); if (pc.AmOwner) Achievements.Type.OutOfTime.Complete(); @@ -102,30 +104,33 @@ public class Duality : RoleBase Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); } + public override void OnReportDeadBody() + { + Timer.Reset(); + } + public override void AfterMeetingTasks() { - if (TimerEndTS - Utils.TimeStamp < 8) - { - TimerEndTS = Utils.TimeStamp + 8; - Utils.SendRPC(CustomRPC.SyncRoleData, DualityId, TimerEndTS, KillingPhase); - } + ResetTimer(); } void ResetTimer() { - TimerEndTS = Utils.TimeStamp + Time.GetInt() + (Main.IntroDestroyed ? 1 : 12); - Utils.SendRPC(CustomRPC.SyncRoleData, DualityId, TimerEndTS, KillingPhase); + Timer = Stopwatch.StartNew(); + Utils.SendRPC(CustomRPC.SyncRoleData, DualityId, KillingPhase); } + long GetRemainingTime() => Time.GetInt() - Timer.Elapsed.Seconds; + public void ReceiveRPC(MessageReader reader) { - TimerEndTS = long.Parse(reader.ReadString()); KillingPhase = reader.ReadBoolean(); + Timer = Stopwatch.StartNew(); } public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) { if (seer.PlayerId != DualityId || seer.PlayerId != target.PlayerId || (!hud && seer.IsModdedClient()) || meeting || !seer.IsAlive()) return string.Empty; - return $"{string.Format(Translator.GetString(KillingPhase ? "Duality.MustKill" : "Duality.MustDoTask"), TimerEndTS - Utils.TimeStamp)}"; + return $"{string.Format(Translator.GetString(KillingPhase ? "Duality.MustKill" : "Duality.MustDoTask"), GetRemainingTime() - 1)}"; } } \ No newline at end of file diff --git a/Roles/Neutral/Shifter.cs b/Roles/Neutral/Shifter.cs index 441c33bcb..a2d72680e 100644 --- a/Roles/Neutral/Shifter.cs +++ b/Roles/Neutral/Shifter.cs @@ -83,8 +83,6 @@ public class Shifter : RoleBase CustomRoles targetRole = target.GetCustomRole(); - Utils.RemovePlayerFromPreviousRoleData(target); - killer.RpcSetCustomRole(targetRole); killer.RpcChangeRoleBasis(targetRole); diff --git a/Roles/Neutral/Thief.cs b/Roles/Neutral/Thief.cs index 40f4295d2..f9983fc2e 100644 --- a/Roles/Neutral/Thief.cs +++ b/Roles/Neutral/Thief.cs @@ -8,6 +8,7 @@ public class Thief : RoleBase private static OptionItem CanStealCovenRoles; private static OptionItem ImpostorVision; + private static OptionItem CanKillRound1; public override bool IsEnable => On; @@ -15,7 +16,8 @@ public class Thief : RoleBase { StartSetup(656400) .AutoSetupOption(ref CanStealCovenRoles, false) - .AutoSetupOption(ref ImpostorVision, false); + .AutoSetupOption(ref ImpostorVision, false) + .AutoSetupOption(ref CanKillRound1, false); } public override void Init() @@ -30,7 +32,7 @@ public class Thief : RoleBase public override bool CanUseKillButton(PlayerControl pc) { - return pc.IsAlive(); + return !MeetingStates.FirstMeeting || CanKillRound1.GetBool(); } public override void ApplyGameOptions(IGameOptions opt, byte playerId)