diff --git a/CTA/CustomTeamAssigner/CustomRoles.cs b/CTA/CustomTeamAssigner/CustomRoles.cs index add07710e..129c2e6c0 100644 --- a/CTA/CustomTeamAssigner/CustomRoles.cs +++ b/CTA/CustomTeamAssigner/CustomRoles.cs @@ -358,7 +358,7 @@ public enum CustomRoles Sprayer, DarkHide, // Stalker Starspawn, - Succubus, + Succubus, // Cultist Sunnyboy, Technician, Tank, @@ -477,6 +477,7 @@ public enum CustomRoles Circumvent, Cleansed, Clumsy, + Commited, Contagious, Damocles, Deadlined, diff --git a/CustomRoles.cs b/CustomRoles.cs index 6dcceb776..6ba673e75 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -358,7 +358,7 @@ public enum CustomRoles Sprayer, DarkHide, // Stalker Starspawn, - Succubus, + Succubus, // Cultist Sunnyboy, Technician, Tank, @@ -477,6 +477,7 @@ public enum CustomRoles Circumvent, Cleansed, Clumsy, + Commited, Contagious, Damocles, Deadlined, diff --git a/GameMode/BedWars.cs b/GameMode/BedWars.cs index fe213a42a..6a244f002 100644 --- a/GameMode/BedWars.cs +++ b/GameMode/BedWars.cs @@ -728,7 +728,7 @@ public static class BedWars { if (pc == null) yield break; - pc.Notify(string.Format(Translator.GetString("Bedwars.ReviveCountdown"), time), overrideAll: true); + pc.Notify(string.Format(Translator.GetString("Bedwars.ReviveCountdown"), time), overrideAll: true, sendOption: SendOption.None); yield return new WaitForSeconds(1f); time--; } @@ -736,9 +736,17 @@ public static class BedWars Health = MaxHealth; NameNotifyManager.Notifies.Remove(pc.PlayerId); RPC.PlaySoundRPC(pc.PlayerId, Sounds.TaskComplete); - pc.ReviveFromTemporaryExile(); - pc.RpcSetColor(Team.GetColorId()); + pc.ReviveFromTemporaryExile(sender => + { + sender.AutoStartRpc(pc.NetId, RpcCalls.SetColor) + .Write(pc.Data.NetId) + .Write(Team.GetColorId()) + .EndRpc(); + + return true; + }); pc.TP(Base.SpawnPosition); + pc.SetChatVisible(true); Utils.NotifyRoles(SpecifyTarget: pc, SendOption: SendOption.None); Reviving.Remove(pc.PlayerId); @@ -746,13 +754,13 @@ public static class BedWars public string BuildSuffix(PlayerControl pc) { - if (!pc.IsAlive()) return string.Empty; - var sb = new StringBuilder(); if (NameNotifyManager.GetNameNotify(pc, out string notify) && notify.Length > 0) sb.AppendLine(notify); + if (!pc.IsAlive()) return $"<#ffffff>{sb.ToString().Trim()}"; + sb.AppendLine(IsGracePeriod ? Translator.GetString("Bedwars.GracePeriod") : GetHealthInfo()); var topLines = 12; @@ -844,13 +852,13 @@ public static class BedWars { [BedWarsTeam.Blue] = new(new(-20.29f, -5.31f), SystemTypes.Reactor, new(-21.61f, -8.2f), new(-21.47f, -2.24f), new(-22.56f, -2.91f)), [BedWarsTeam.Yellow] = new(new(-9.04f, -1.79f), SystemTypes.MedBay, new(-7.68f, -3.7f), new(-7.68f, -5.18f), new(-5.67f, -5.31f)), - [BedWarsTeam.Red] = new(new(5.08f, -14.71f), SystemTypes.Comms, new(2.72f, -14.76f), new(2.72f, -17.05f), new(5.63f, -16.71f)), + [BedWarsTeam.Red] = new(new(5.08f, -14.71f), SystemTypes.Comms, new(2.72f, -14.76f), new(2.72f, -16.37f), new(5.63f, -16.71f)), [BedWarsTeam.Green] = new(new(14.54f, -4.61f), SystemTypes.Nav, new(16.57f, -3.21f), new(16.57f, -6.12f), new(17.57f, -4.07f)) }, [MapNames.MiraHQ] = new() { [BedWarsTeam.Blue] = new(new(2.81f, -1.43f), SystemTypes.Launchpad, new(-4.46f, 0.48f), new(-4.46f, 4.12f), new(-4.45f, 2.4f)), - [BedWarsTeam.Yellow] = new(new(22.16f, 1.5f), SystemTypes.Balcony, new(20.02f, -1.81f), new(27.62f, -1.81f), new(23.77f, -1.58f)), + [BedWarsTeam.Yellow] = new(new(22.37f, 1.5f), SystemTypes.Balcony, new(20.02f, -1.81f), new(27.62f, -1.81f), new(23.77f, -1.58f)), [BedWarsTeam.Red] = new(new(17.8f, 23.17f), SystemTypes.Greenhouse, new(13.58f, 22.4f), new(22.08f, 22.37f), new(17.85f, 25.55f)), [BedWarsTeam.Green] = new(new(6.12f, 12.12f), SystemTypes.Reactor, new(1.69f, 10.63f), new(8.88f, 12.38f), new(2.47f, 11.9f)) }, @@ -1202,7 +1210,7 @@ public static class BedWars [Item.WoodenSword] = (Item.Iron, 1), [Item.IronSword] = (Item.Gold, 3), - [Item.DiamondSword] = (Item.Emerald, 1), + [Item.DiamondSword] = (Item.Emerald, 2), [Item.IronArmor] = (Item.Gold, 6), [Item.DiamondArmor] = (Item.Emerald, 4), @@ -1397,7 +1405,7 @@ public static class BedWars [Item.DiamondSword] = new(Color.cyan, '✽', () => Translator.GetString("BedWars.Item.DiamondSword")), [Item.IronArmor] = new(Color.white, '◎', () => Translator.GetString("BedWars.Item.IronArmor")), - [Item.DiamondArmor] = new(Color.cyan, '◍', () => Translator.GetString("BedWars.Item.DiamondArmor")), + [Item.DiamondArmor] = new(Color.cyan, '⊗', () => Translator.GetString("BedWars.Item.DiamondArmor")), [Item.Pickaxe] = new(new Color32(85, 92, 133, 255), '┯', () => Translator.GetString("BedWars.Item.Pickaxe")), [Item.Axe] = new(new Color32(108, 112, 77, 255), '┭', () => Translator.GetString("BedWars.Item.Axe")), @@ -1530,7 +1538,7 @@ public static class BedWars LateTask.New(() => { if (GameStates.IsEnded || !GameStates.InGame || GameStates.IsLobby) return; - Main.AllPlayerSpeed[pc.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.KillCooldown); + Main.AllPlayerSpeed[pc.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); pc.MarkDirtySettings(); }, SpeedPotionDuration); break; @@ -1708,7 +1716,7 @@ public static class BedWars if (newStr != str) pc.Notify(newStr, 100f, true); } - if (Vector2.Distance(pc.Pos(), Position) > BedBreakAndProtectRange) + if (!pc.IsAlive() || Vector2.Distance(pc.Pos(), Position) > BedBreakAndProtectRange) { Breaking.Remove(pc.PlayerId); yield break; @@ -1721,20 +1729,13 @@ public static class BedWars data.Inventory.Adjust(topLayer, GetNextProtectReq()); UpdateStatus(); Breaking.Remove(pc.PlayerId); - if (Layers.Count == 0) Broken(); + if (Layers.Count == 0 && data.Team != Team) Broken(); } } private void Broken() { - BedWarsTeam team = this switch - { - BlueBed => BedWarsTeam.Blue, - YellowBed => BedWarsTeam.Yellow, - GreenBed => BedWarsTeam.Green, - RedBed => BedWarsTeam.Red, - _ => throw new InvalidOperationException("Unknown bed type") - }; + BedWarsTeam team = Team; foreach ((byte id, PlayerData data) in Data) { @@ -1748,6 +1749,15 @@ public static class BedWars Despawn(); IsBroken = true; } + + private BedWarsTeam Team => this switch + { + BlueBed => BedWarsTeam.Blue, + YellowBed => BedWarsTeam.Yellow, + GreenBed => BedWarsTeam.Green, + RedBed => BedWarsTeam.Red, + _ => throw new InvalidOperationException("Unknown bed type") + }; } public static void OnTNTExplode(Vector2 position) diff --git a/GameMode/CaptureTheFlag.cs b/GameMode/CaptureTheFlag.cs index 02ece154f..c05fa2225 100644 --- a/GameMode/CaptureTheFlag.cs +++ b/GameMode/CaptureTheFlag.cs @@ -752,7 +752,6 @@ public static class CaptureTheFlag TemporarilyOutPlayers.Remove(__instance.PlayerId); __instance.ReviveFromTemporaryExile(); __instance.TP(team.GetFlagBase().Position); - __instance.SetKillCooldown(); RPC.PlaySoundRPC(__instance.PlayerId, Sounds.TaskComplete); Utils.NotifyRoles(SpecifySeer: __instance, SpecifyTarget: __instance, SendOption: SendOption.None); } diff --git a/GameMode/KingOfTheZones.cs b/GameMode/KingOfTheZones.cs index 841f9f698..22d57aab7 100644 --- a/GameMode/KingOfTheZones.cs +++ b/GameMode/KingOfTheZones.cs @@ -789,7 +789,7 @@ public static class KingOfTheZones player.ReviveFromTemporaryExile(); player.TP(RandomSpawn.SpawnMap.GetSpawnMap().Positions.ExceptBy(Zones, x => x.Key).RandomElement().Value); - player.SetKillCooldown(GetKillCooldown(player)); + LateTask.New(() => player.SetKillCooldown(GetKillCooldown(player)), 1.5f, log: false); RPC.PlaySoundRPC(player.PlayerId, Sounds.TaskComplete); Utils.NotifyRoles(SpecifyTarget: player, SendOption: SendOption.None); diff --git a/GameMode/NaturalDisasters.cs b/GameMode/NaturalDisasters.cs index 21913f60f..2f6f084c5 100644 --- a/GameMode/NaturalDisasters.cs +++ b/GameMode/NaturalDisasters.cs @@ -961,6 +961,8 @@ public static class NaturalDisasters foreach (PlayerControl pc in Main.AllAlivePlayerControls) { + if (pc.onLadder || pc.inMovingPlat || pc.MyPhysics.Animations.IsPlayingAnyLadderAnimation()) continue; + PlainShipRoom room = pc.GetPlainShipRoom(); if (room != null && CollapsedRooms.Exists(x => x == room)) diff --git a/Main.cs b/Main.cs index 49b2b8922..860a0f2ef 100644 --- a/Main.cs +++ b/Main.cs @@ -127,6 +127,7 @@ public class Main : BasePlugin public static readonly Dictionary GamesPlayed = []; public static readonly HashSet GotShieldAnimationInfoThisGame = []; public static readonly HashSet Invisible = []; + public static readonly Dictionary UserData = []; public static readonly Dictionary> HasPlayedGM = new() { @@ -150,7 +151,9 @@ public class Main : BasePlugin public static Dictionary KilledAntidote = []; public static List BrakarVoteFor = []; public static Dictionary SleuthMsgs = []; + public static Dictionary NumEmergencyMeetingsUsed = []; public static int MadmateNum; + public static uint LobbyBehaviourNetId; public static bool ShowResult = true; @@ -629,6 +632,7 @@ public class Main : BasePlugin { CustomRoles.Messenger, "#28b573" }, { CustomRoles.Dynamo, "#ebe534" }, { CustomRoles.AntiTP, "#fcba03" }, + { CustomRoles.Commited, "#f5c542" }, { CustomRoles.BananaMan, "#ffe135" }, { CustomRoles.Blind, "#666666" }, { CustomRoles.Shy, "#9582f5" }, diff --git a/Modules/AntiBlackout.cs b/Modules/AntiBlackout.cs index 0506e3a9e..a2185792d 100644 --- a/Modules/AntiBlackout.cs +++ b/Modules/AntiBlackout.cs @@ -124,7 +124,7 @@ public static class AntiBlackout { SkipTasks = false; ExileControllerWrapUpPatch.AfterMeetingTasks(); - }, Math.Max(1f, Utils.CalculatePingDelay() * 2f), "Reset SkipTasks after SetRealPlayerRoles"); + }, Math.Min(3f, Math.Max(1f, Utils.CalculatePingDelay() * 2f)), "Reset SkipTasks after SetRealPlayerRoles"); }, 0.2f, "SetRealPlayerRoles - Reset Cooldowns"); } diff --git a/Modules/AutoHaunt.cs b/Modules/AutoHaunt.cs index ffb92af89..8568e0e7c 100644 --- a/Modules/AutoHaunt.cs +++ b/Modules/AutoHaunt.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using AmongUs.GameOptions; using EHR.GameMode.HideAndSeekRoles; using UnityEngine; @@ -37,13 +38,18 @@ public static class AutoHaunt { while (Main.AutoHaunt.Value) { - if (GameStates.IsInTask && !ExileController.Instance && !AntiBlackout.SkipTasks && !PlayerControl.LocalPlayer.IsAlive() && HauntMenuMinigameStartPatch.Instance != null) + if (GameStates.IsInTask && !ExileController.Instance && !AntiBlackout.SkipTasks && !PlayerControl.LocalPlayer.IsAlive() && PlayerControl.LocalPlayer.Data.RoleType is RoleTypes.CrewmateGhost or RoleTypes.ImpostorGhost) { - PlayerControl currentTarget = HauntMenuMinigameStartPatch.Instance.HauntTarget; - PlayerControl preferredTarget = GetPreferredHauntTarget(); + if (HauntMenuMinigameStartPatch.Instance != null) + { + PlayerControl currentTarget = HauntMenuMinigameStartPatch.Instance.HauntTarget; + PlayerControl preferredTarget = GetPreferredHauntTarget(); - if (preferredTarget != null && currentTarget != preferredTarget) - HauntMenuMinigameSetHauntTargetPatch.Prefix(HauntMenuMinigameStartPatch.Instance, preferredTarget); + if (preferredTarget != null && currentTarget != preferredTarget) + HauntMenuMinigameSetHauntTargetPatch.Prefix(HauntMenuMinigameStartPatch.Instance, preferredTarget); + } + else + FastDestroyableSingleton.Instance.AbilityButton.DoClick(); } yield return new WaitForSeconds(5f); diff --git a/Modules/CustomRpcSender.cs b/Modules/CustomRpcSender.cs index 34df51439..e8bf86939 100644 --- a/Modules/CustomRpcSender.cs +++ b/Modules/CustomRpcSender.cs @@ -689,7 +689,7 @@ public static class CustomRpcSenderExtensions { senderWasCleared = false; newSender = sender; - if (seer == null || seer.Data.Disconnected || (seer.IsModdedClient() && (seer.IsHost() || Options.CurrentGameMode == CustomGameMode.Standard)) || (!SetUpRoleTextPatch.IsInIntro && GameStates.IsLobby)) return false; + if (!AmongUsClient.Instance.AmHost || seer == null || seer.Data.Disconnected || (seer.IsModdedClient() && (seer.IsHost() || Options.CurrentGameMode == CustomGameMode.Standard)) || (!SetUpRoleTextPatch.IsInIntro && GameStates.IsLobby)) return false; var hasValue = Utils.WriteSetNameRpcsToSender(ref sender, false, false, false, false, false, false, seer, [seer], [target], out senderWasCleared) && !senderWasCleared; newSender = sender; return hasValue; diff --git a/Modules/CustomSabotage.cs b/Modules/CustomSabotage.cs index 450fd1f20..4811a65cc 100644 --- a/Modules/CustomSabotage.cs +++ b/Modules/CustomSabotage.cs @@ -51,6 +51,8 @@ public abstract class CustomSabotage public static string GetAllSuffix(PlayerControl seer, PlayerControl target, bool hud, bool meeting) { + if (!AmongUsClient.Instance.AmHost) return string.Empty; + StringBuilder suffix = new(); foreach (CustomSabotage sabotage in Instances) @@ -61,7 +63,16 @@ public abstract class CustomSabotage suffix.Append($"{tempSuffix}\n"); } - return suffix.ToString().Trim(); + var result = suffix.ToString(); + + if (seer.IsNonHostModdedClient()) + { + if (NameNotifyManager.GetNameNotify(seer, out string name) && name == result) return string.Empty; + seer.Notify(result, 3f, true, false); + return string.Empty; + } + + return result.Trim(); } public static void UpdateAll() diff --git a/Modules/DisableDevice.cs b/Modules/DisableDevice.cs index 2af9711ee..9e7330c1c 100644 --- a/Modules/DisableDevice.cs +++ b/Modules/DisableDevice.cs @@ -12,7 +12,7 @@ namespace EHR; // Reference: https://github.com/ykundesu/SuperNewRoles/blob/master/SuperNewRoles/Mode/SuperHostRoles/BlockTool.cs internal static class DisableDevice { - private static readonly List DesyncComms = []; + public static readonly List DesyncComms = []; private static int frame; public static readonly Dictionary DevicePos = new() diff --git a/Modules/EAC.cs b/Modules/EAC.cs index 79b846370..81ddc8ceb 100644 --- a/Modules/EAC.cs +++ b/Modules/EAC.cs @@ -246,7 +246,7 @@ internal static class EAC return true; } - if (!resultFlags.HasFlag(MurderResultFlags.FailedError) && !resultFlags.HasFlag(MurderResultFlags.FailedProtected) && target != null) + if (!resultFlags.HasFlag(MurderResultFlags.FailedError) && !resultFlags.HasFlag(MurderResultFlags.FailedProtected) && target != null && !target.Data.IsDead) LateTask.New(() => target.RpcRevive(), 0.1f, log: false); Report(pc, "Directly Murder Player"); diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 5bbd25bb2..20e65b8f4 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using AmongUs.GameOptions; +using EHR.AddOns.Common; using EHR.AddOns.Crewmate; using EHR.AddOns.Impostor; using EHR.Coven; @@ -299,7 +300,7 @@ internal static class ExtendedPlayerControl } // Saves some RPC calls for vanilla servers to make innersloth's rate limit happy - public static void ReviveFromTemporaryExile(this PlayerControl player) // Only used in game modes + public static void ReviveFromTemporaryExile(this PlayerControl player, Func extraWrites = null) // Only used in game modes { if (GameStates.CurrentServerType != GameStates.ServerType.Vanilla) { @@ -313,29 +314,37 @@ internal static class ExtendedPlayerControl state.IsDead = false; state.deathReason = PlayerState.DeathReason.etc; - var sender = CustomRpcSender.Create("ReviveFromTemporaryExile", SendOption.Reliable); - var hasValue = false; - player.RpcSetRoleGlobal(RoleTypes.Crewmate); - RoleTypes newRoleType = state.MainRole.GetRoleTypes(); - - if (Options.CurrentGameMode is CustomGameMode.SoloKombat or CustomGameMode.FFA or CustomGameMode.CaptureTheFlag or CustomGameMode.KingOfTheZones or CustomGameMode.BedWars) - hasValue |= sender.RpcSetRole(player, newRoleType, player.OwnerId); - - player.ResetKillCooldown(); - LateTask.New(() => player.SetKillCooldown(), 0.2f, log: false); - - if (newRoleType is not (RoleTypes.Crewmate or RoleTypes.Impostor or RoleTypes.Noisemaker)) - hasValue |= sender.RpcResetAbilityCooldown(player); - - if (DoRPC) + LateTask.New(() => { - sender.SyncGeneralOptions(player); - hasValue = true; - } + var sender = CustomRpcSender.Create("ReviveFromTemporaryExile", SendOption.Reliable); + var hasValue = false; - sender.SendMessage(!hasValue); + RoleTypes newRoleType = state.MainRole.GetRoleTypes(); + + if (Options.CurrentGameMode is CustomGameMode.SoloKombat or CustomGameMode.FFA or CustomGameMode.CaptureTheFlag or CustomGameMode.KingOfTheZones or CustomGameMode.BedWars) + hasValue |= sender.RpcSetRole(player, newRoleType, player.OwnerId); + + player.ResetKillCooldown(); + + if (Options.CurrentGameMode != CustomGameMode.KingOfTheZones) + LateTask.New(() => player.SetKillCooldown(), 0.2f, log: false); + + if (newRoleType is not (RoleTypes.Crewmate or RoleTypes.Impostor or RoleTypes.Noisemaker)) + hasValue |= sender.RpcResetAbilityCooldown(player); + + if (DoRPC) + { + sender.SyncGeneralOptions(player); + hasValue = true; + } + + if (extraWrites != null) + hasValue |= extraWrites(sender); + + sender.SendMessage(!hasValue); + }, 1f, log: false); } // If you use vanilla RpcSetRole, it will block further SetRole calls until the next game starts. @@ -904,11 +913,14 @@ internal static class ExtendedPlayerControl { if (player == null) return; - Logger.Info($"{player.GetNameWithRole()}'s KCD set to {(Math.Abs(time - -1f) < 0.5f ? Main.AllPlayerKillCooldown[player.PlayerId] : time)}s", "SetKCD"); + if (!Mathf.Approximately(time, -1f) && Commited.ReduceKCD.TryGetValue(player.PlayerId, out float reduction)) + time = Math.Max(time - reduction, 0.01f); + + Logger.Info($"{player.GetNameWithRole()}'s KCD set to {(time < 0f ? Main.AllPlayerKillCooldown[player.PlayerId] : time)}s", "SetKCD"); if (player.GetCustomRole().UsesPetInsteadOfKill()) { - if (Math.Abs(time - -1f) < 0.5f) + if (time < 0f) player.AddKCDAsAbilityCD(); else player.AddAbilityCD((int)Math.Round(time)); @@ -1714,6 +1726,9 @@ internal static class ExtendedPlayerControl Logger.Info($"KCD of player set to {Main.AllPlayerKillCooldown[player.PlayerId]}", "Antidote"); } + if (Commited.ReduceKCD.TryGetValue(player.PlayerId, out float reduction)) + Main.AllPlayerKillCooldown[player.PlayerId] -= reduction; + if (sync) player.SyncSettings(); } @@ -2168,6 +2183,7 @@ internal static class ExtendedPlayerControl if (ChatCommands.IsPlayerModerator(pc.FriendCode)) return true; if (ChatCommands.IsPlayerVIP(pc.FriendCode)) return true; if (PrivateTagManager.Tags.ContainsKey(pc.FriendCode)) return true; + if (Main.UserData.TryGetValue(pc.FriendCode, out Options.UserData userData) && !string.IsNullOrWhiteSpace(userData.Tag) && userData.Tag.Length > 0) return true; ClientData client = pc.GetClient(); return client != null && FastDestroyableSingleton.Instance.IsPlayerFriend(client.ProductUserId); diff --git a/Modules/FixedUpdateCaller.cs b/Modules/FixedUpdateCaller.cs index b0b316cda..95f68cd83 100644 --- a/Modules/FixedUpdateCaller.cs +++ b/Modules/FixedUpdateCaller.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using EHR.Crewmate; using EHR.Patches; using HarmonyLib; using InnerNet; @@ -95,6 +96,12 @@ public static class FixedUpdateCaller } catch { } + try + { + if (CopyCat.Instances.Count > 0) CopyCat.Instances.RemoveAll(x => x.CopyCatPC == null); + } + catch { } + 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 209d37fa7..67d176ee7 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -368,7 +368,7 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS case CustomRoles.Mare when Options.MareHasIncreasedSpeed.GetBool(): Main.AllPlayerSpeed[player.PlayerId] = Options.MareSpeedDuringLightsOut.GetFloat(); break; - case CustomRoles.Sleep when Utils.IsActive(SystemTypes.Electrical): + case CustomRoles.Sleep when player.IsAlive() && Utils.IsActive(SystemTypes.Electrical): SetBlind(); Main.AllPlayerSpeed[player.PlayerId] = Main.MinSpeed; break; diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs index 3d1ae4eef..5d31f4192 100644 --- a/Modules/GuessManager.cs +++ b/Modules/GuessManager.cs @@ -461,6 +461,8 @@ public static class GuessManager Main.GuesserGuessed[pc.PlayerId]++; Main.GuesserGuessedMeeting[pc.PlayerId]++; + if (pc.IsHost()) Utils.FlashColor(guesserSuicide ? new(1f, 0f, 0f, 0.3f) : new(0f, 1f, 0f, 0.3f)); + if (Main.PlayerStates[pc.PlayerId].Role is NecroGuesser ng) { if (!guesserSuicide) ng.GuessedPlayers++; diff --git a/Modules/NameColorManager.cs b/Modules/NameColorManager.cs index 532e0f869..80252921d 100644 --- a/Modules/NameColorManager.cs +++ b/Modules/NameColorManager.cs @@ -140,6 +140,7 @@ public static class NameColorManager // Check if the seer can see the target's role color color = seerRole switch { + CustomRoles.PlagueBearer when PlagueBearer.IsPlagued(seer.PlayerId, target.PlayerId) => "000000", CustomRoles.Executioner when Executioner.Target.TryGetValue(seer.PlayerId, out byte exeTarget) && exeTarget == target.PlayerId => "000000", CustomRoles.Gangster when target.Is(CustomRoles.Madmate) => Main.RoleColors[CustomRoles.Madmate], CustomRoles.Crewpostor when target.Is(CustomRoleTypes.Impostor) && Options.CrewpostorKnowsAllies.GetBool() => Main.ImpostorColor, diff --git a/Modules/NameNotifyManager.cs b/Modules/NameNotifyManager.cs index 8191b7ca5..3c9697a45 100644 --- a/Modules/NameNotifyManager.cs +++ b/Modules/NameNotifyManager.cs @@ -16,7 +16,7 @@ public static class NameNotifyManager Notifies = []; } - public static void Notify(this PlayerControl pc, string text, float time = 6f, bool overrideAll = false, bool log = true) + public static void Notify(this PlayerControl pc, string text, float time = 6f, bool overrideAll = false, bool log = true, SendOption sendOption = SendOption.Reliable) { if (!AmongUsClient.Instance.AmHost || pc == null) return; if (!GameStates.IsInTask) return; @@ -30,8 +30,8 @@ public static class NameNotifyManager else notifies[text] = expireTS; - if (pc.IsNonHostModdedClient()) SendRPC(pc.PlayerId, text, expireTS, overrideAll); - Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); + if (pc.IsNonHostModdedClient()) SendRPC(pc.PlayerId, text, expireTS, overrideAll, sendOption); + Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc, SendOption: sendOption); if (log) Logger.Info($"New name notify for {pc.GetNameWithRole().RemoveHtmlTags()}: {text} ({time}s)", "Name Notify"); } @@ -73,11 +73,11 @@ public static class NameNotifyManager return true; } - private static void SendRPC(byte playerId, string text, long expireTS, bool overrideAll) // Only sent when adding a new notification + private static void SendRPC(byte playerId, string text, long expireTS, bool overrideAll, SendOption sendOption) // Only sent when adding a new notification { if (!AmongUsClient.Instance.AmHost) return; - MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncNameNotify, SendOption.Reliable); + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncNameNotify, sendOption); writer.Write(playerId); writer.Write(text); writer.Write(expireTS.ToString()); diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index c6c625dbd..081a0ba1e 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -2,8 +2,10 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Reflection; +using System.Text.Json; using EHR.AddOns; using EHR.AddOns.GhostRoles; using EHR.Modules; @@ -173,7 +175,9 @@ public static class Options public static readonly string[] PetToAssign = [ + "pet_Goose", "pet_Bedcrab", + "pet_DancingSkeletonPet", "pet_BredPet", "pet_YuleGoatPet", "pet_Bush", @@ -181,9 +185,12 @@ public static class Options "pet_ChewiePet", "pet_clank", "pet_coaltonpet", + "pet_Creb", "pet_Cube", + "pet_lny_dragon", "pet_Doggy", "pet_Ellie", + "pet_Strawb", "pet_frankendog", "pet_D2GhostPet", "pet_test", @@ -194,13 +201,23 @@ public static class Options "pet_Alien", "pet_poro", "pet_HamPet", + "pet_Crow", "pet_Lava", "pet_Crewmate", + "pet_Mister", + "pet_nancy", + "pet_napstamate", + "pet_Pip", + "pet_pocketCircuitCar", "pet_D2PoukaPet", "pet_Pusheen", + "pet_Pate", + "pet_Rammy", "pet_Robot", "pet_Snow", + "pet_spaceCat", "pet_Squig", + "pet_Stormy", "pet_nuggetPet", "pet_Charles_Red", "pet_UFO", @@ -907,6 +924,7 @@ public static class Options Logger.Info("Options.Load End", "Options"); GroupOptions(); GroupAddons(); + LoadUserData(); Achievements.LoadAllData(); OptionShower.LastText = Translator.GetString("Loading"); @@ -1127,6 +1145,48 @@ public static class Options .ToDictionary(x => x.Key, x => x.Select(y => Enum.Parse(y.GetType().Name, true)).ToList()); } + public static void LoadUserData() + { + try + { + Main.UserData.Clear(); + + var path = $"{Main.DataPath}/EHR_DATA/UserData"; + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + File.WriteAllText(path + "/friendcode#1234.txt", JsonSerializer.Serialize(new UserData(), new JsonSerializerOptions { WriteIndented = true })); + } + + foreach (string file in Directory.GetFiles(path, "*.txt")) + { + try + { + string content = File.ReadAllText(file); + var userData = JsonSerializer.Deserialize(content); + if (userData == null) throw new FormatException($"The data in {file} was not in the correct format."); + string fileName = Path.GetFileNameWithoutExtension(file); + Main.UserData[fileName] = userData; + } + catch (Exception e) + { + Logger.Error($"Failed to load user data from {file}", "Options"); + Utils.ThrowException(e); + } + } + } + catch (Exception e) { Utils.ThrowException(e); } + } + + public class UserData + { + public bool Vip { get; init; } + public bool Moderator { get; init; } + public bool Admin { get; init; } + public string Tag { get; init; } + } + public static VoteMode GetWhenSkipVote() { return (VoteMode)WhenSkipVote.GetValue(); @@ -2280,7 +2340,7 @@ public static class Options .SetHeader(true) .SetColor(new Color32(60, 0, 255, byte.MaxValue)); - PetToAssignToEveryone = new StringOptionItem(23854, "PetToAssign", PetToAssign, 24, TabGroup.TaskSettings) + PetToAssignToEveryone = new StringOptionItem(23854, "PetToAssign", PetToAssign, 18, TabGroup.TaskSettings) .SetParent(UsePets) .SetColor(new Color32(60, 0, 255, byte.MaxValue)); diff --git a/Modules/Utils.cs b/Modules/Utils.cs index e2677c6dd..97ed681ce 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -2068,8 +2068,9 @@ public static class Utils bool vip = ChatCommands.IsPlayerVIP(player.FriendCode); bool hasTag = devUser.HasTag(); bool hasPrivateTag = PrivateTagManager.Tags.TryGetValue(player.FriendCode, out string privateTag); + bool hasTagInUserData = Main.UserData.TryGetValue(player.FriendCode, out Options.UserData userData) && !string.IsNullOrWhiteSpace(userData.Tag) && userData.Tag.Length > 0; - if (!player.AmOwner && !hasTag && !mod && !vip && !hasPrivateTag && !DirtyName.Contains(player.PlayerId)) return false; + if (!player.AmOwner && !hasTag && !mod && !vip && !hasPrivateTag && !hasTagInUserData && !DirtyName.Contains(player.PlayerId)) return false; if (!Main.AllPlayerNames.TryGetValue(player.PlayerId, out name)) return false; if (Main.NickName != string.Empty && player.AmOwner) name = Main.NickName; @@ -2114,9 +2115,9 @@ public static class Utils }; } - if (hasTag || mod || vip || hasPrivateTag) + if (hasTag || mod || vip || hasPrivateTag || hasTagInUserData) { - string pTag = hasPrivateTag ? privateTag : string.Empty; + string pTag = hasPrivateTag ? privateTag : hasTagInUserData ? userData.Tag : string.Empty; string tag = hasTag ? devUser.GetTag() : string.Empty; if (tag == "null") tag = string.Empty; @@ -2332,7 +2333,7 @@ public static class Utils public static IEnumerator NotifyEveryoneAsync(int speed = 2, bool noCache = true) { - if (GameStates.IsMeeting) yield break; + if (!AmongUsClient.Instance.AmHost || GameStates.IsMeeting) yield break; var count = 0; PlayerControl[] aapc = Main.AllAlivePlayerControls; @@ -2355,7 +2356,8 @@ public static class Utils { try { - if (!SetUpRoleTextPatch.IsInIntro && ((SpecifySeer != null && SpecifySeer.IsModdedClient() && (Options.CurrentGameMode == CustomGameMode.Standard || SpecifySeer.IsHost())) || !AmongUsClient.Instance.AmHost || (GameStates.IsMeeting && !ForMeeting) || GameStates.IsLobby)) return; + if (!AmongUsClient.Instance.AmHost) return; + if (!SetUpRoleTextPatch.IsInIntro && ((SpecifySeer != null && SpecifySeer.IsModdedClient() && (Options.CurrentGameMode == CustomGameMode.Standard || SpecifySeer.IsHost())) || (GameStates.IsMeeting && !ForMeeting) || GameStates.IsLobby)) return; PlayerControl[] apc = Main.AllPlayerControls; PlayerControl[] seerList = SpecifySeer != null ? [SpecifySeer] : apc; @@ -2714,6 +2716,7 @@ public static class Utils TargetMark.Append(Witch.GetSpelledMark(target.PlayerId, forMeeting)); if (forMeeting) TargetMark.Append(Wasp.GetStungMark(target.PlayerId)); if (forMeeting) TargetMark.Append(SpellCaster.HasSpelledMark(seer.PlayerId) ? ColorString(Team.Coven.GetColor(), "\u25c0") : string.Empty); + if (forMeeting) TargetMark.Append(Commited.GetMark(seer, target)); if (target.Is(CustomRoles.SuperStar) && Options.EveryOneKnowSuperStar.GetBool()) TargetMark.Append(ColorString(GetRoleColor(CustomRoles.SuperStar), "★")); @@ -2731,7 +2734,6 @@ public static class Utils { case CustomRoles.PlagueBearer when PlagueBearer.IsPlagued(seer.PlayerId, target.PlayerId): TargetMark.Append($"●"); - PlagueBearer.SendRPC(seer, target); break; case CustomRoles.Arsonist: if (seer.IsDousedPlayer(target)) @@ -3005,6 +3007,8 @@ public static class Utils public static bool RpcChangeSkin(PlayerControl pc, NetworkedPlayerInfo.PlayerOutfit newOutfit, CustomRpcSender writer = null, SendOption sendOption = SendOption.Reliable) { + if (!AmongUsClient.Instance.AmHost) return false; + if (pc.Is(CustomRoles.BananaMan)) newOutfit = BananaMan.GetOutfit(Main.AllPlayerNames.GetValueOrDefault(pc.PlayerId, "Banana")); @@ -3364,6 +3368,20 @@ public static class Utils CustomNetObject.AfterMeeting(); RPCHandlerPatch.RemoveExpiredWhiteList(); + + LateTask.New(() => + { + if (GameStates.IsEnded) return; + MessageWriter writer = MessageWriter.Get(SendOption.Reliable); + writer.StartMessage(5); + writer.Write(AmongUsClient.Instance.GameId); + writer.StartMessage(5); + writer.WritePacked(Main.LobbyBehaviourNetId); + writer.EndMessage(); + writer.EndMessage(); + AmongUsClient.Instance.SendOrDisconnect(writer); + writer.Recycle(); + }, 3f, "Repeat Lobby Despawn"); } public static void AfterPlayerDeathTasks(PlayerControl target, bool onMeeting = false, bool disconnect = false) diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 2016fc9dd..43f2d4115 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using AmongUs.GameOptions; using Assets.CoreScripts; @@ -14,6 +15,7 @@ using HarmonyLib; using Hazel; using InnerNet; using UnityEngine; +using UnityEngine.Networking; using static EHR.Translator; // ReSharper disable InconsistentNaming @@ -216,6 +218,8 @@ internal static class ChatCommands new(["vs", "votestart", "голосованиестарт", "投票开始"], "", GetString("CommandDescription.VoteStart"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, VoteStartCommand, true, false), new(["imitate", "имитировать", "模仿"], "{id}", GetString("CommandDescription.Imitate"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, ImitateCommand, true, true, [GetString("CommandArgs.Imitate.Id")]), new(["retribute", "воздать", "报复"], "{id}", GetString("CommandDescription.Retribute"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, RetributeCommand, true, true, [GetString("CommandArgs.Retribute.Id")]), + + new(["confirmauth"], "{uuid}", GetString("CommandDescription.ConfirmAuth"), Command.UsageLevels.Everyone, Command.UsageTimes.Always, ConfirmAuthCommand, true, false, [GetString("CommandArgs.ConfirmAuth.UUID")]), // Commands with action handled elsewhere new(["shoot", "guess", "bet", "bt", "st", "угадать", "бт", "猜测", "赌", "adivinhar"], "{id} {role}", GetString("CommandDescription.Guess"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, (_, _, _) => { }, true, false, [GetString("CommandArgs.Guess.Id"), GetString("CommandArgs.Guess.Role")]), @@ -243,6 +247,9 @@ internal static class ChatCommands if (friendCode == "" || friendCode == string.Empty || !Options.ApplyModeratorList.GetBool()) return false; + if (Main.UserData.TryGetValue(friendCode, out Options.UserData userData) && userData.Moderator) + return true; + long now = Utils.TimeStamp; string[] friendCodes; @@ -272,6 +279,9 @@ internal static class ChatCommands if (friendCode == "" || friendCode == string.Empty || !Options.ApplyVIPList.GetBool()) return false; + if (Main.UserData.TryGetValue(friendCode, out Options.UserData userData) && userData.Vip) + return true; + long now = Utils.TimeStamp; string[] friendCodes; @@ -301,6 +311,9 @@ internal static class ChatCommands if (friendCode == "" || friendCode == string.Empty || !Options.ApplyAdminList.GetBool()) return false; + if (Main.UserData.TryGetValue(friendCode, out Options.UserData userData) && userData.Admin) + return true; + long now = Utils.TimeStamp; string[] friendCodes; @@ -468,7 +481,76 @@ internal static class ChatCommands // --------------------------------------------------------------------------------------------------------------------------------------------- - private static void RetributeCommand(PlayerControl player, string text, string[] args) + private static void ConfirmAuthCommand(PlayerControl player, string text, string[] args) + { + if (GameStates.CurrentServerType != GameStates.ServerType.Vanilla) + { + Utils.SendMessage("\n", player.PlayerId, GetString("ConfirmAuth.ErrorNotVanilla")); + return; + } + + if (!Options.PostLobbyCodeToEHRWebsite.GetBool()) + { + Utils.SendMessage("\n", player.PlayerId, string.Format(GetString("ConfirmAuth.ErrorLobbyUnlisted"), GetString("PostLobbyCodeToEHRDiscordServer"))); + return; + } + + if (args.Length >= 2) + { + string uuid = args[1].Trim(); + if (string.IsNullOrEmpty(uuid)) return; + + Main.Instance.StartCoroutine(SendVerificationCoroutine(uuid)); + } + + return; + + IEnumerator SendVerificationCoroutine(string uuid) + { + string friendCode = player.FriendCode; + string puid = player.GetClient().ProductUserId; + var gameId = AmongUsClient.Instance.GameId.ToString(); + + if (string.IsNullOrEmpty(friendCode) || string.IsNullOrEmpty(puid)) + { + Logger.Error($" Missing friendcode/puid for player {player.PlayerId}", "ConfirmAuth"); + yield break; + } + + if (string.IsNullOrEmpty(gameId) || gameId == "32") + { + Logger.Error(" Invalid GameId", "ConfirmAuth"); + yield break; + } + + var json = $"{{\"uuid\":\"{uuid}\",\"friend_code\":\"{friendCode}\",\"puid\":\"{puid}\",\"game_id\":\"{gameId}\"}}"; + byte[] bodyRaw = Encoding.UTF8.GetBytes(json); + + var uwr = new UnityWebRequest("https://gurge44.pythonanywhere.com/api/verify_ingame", "POST") + { + uploadHandler = new UploadHandlerRaw(bodyRaw), + downloadHandler = new DownloadHandlerBuffer() + }; + uwr.SetRequestHeader("Content-Type", "application/json"); + uwr.SetRequestHeader("User-Agent", $"{Main.ModName} v{Main.PluginVersion}"); + uwr.timeout = 10; // seconds + + yield return uwr.SendWebRequest(); + + if (uwr.result != UnityWebRequest.Result.Success) + { + Logger.Error($" HTTP error sending verification: {uwr.error} (code {(int)uwr.responseCode})", "ConfirmAuth"); + Utils.SendMessage("\n", player.PlayerId, string.Format(GetString("ConfirmAuth.Error"), uwr.error)); + } + else + { + Logger.Msg($" Sent ok. Resp code {(int)uwr.responseCode}. Body: {uwr.downloadHandler?.text}", "ConfirmAuth"); + Utils.SendMessage("\n", player.PlayerId, GetString("ConfirmAuth.Success")); + } + } + } + + public static void RetributeCommand(PlayerControl player, string text, string[] args) { if (Starspawn.IsDayBreak) return; @@ -2809,21 +2891,25 @@ internal static class ChatCommands switch (subArgs) { + case "skeld": case "theskeld": GameOptionsManager.Instance.CurrentGameOptions.SetByte(ByteOptionNames.MapId, 0); break; + case "mira": case "mirahq": GameOptionsManager.Instance.CurrentGameOptions.SetByte(ByteOptionNames.MapId, 1); break; case "polus": GameOptionsManager.Instance.CurrentGameOptions.SetByte(ByteOptionNames.MapId, 2); break; + case "dleks": case "dlekseht": GameOptionsManager.Instance.CurrentGameOptions.SetByte(ByteOptionNames.MapId, 3); break; case "airship": GameOptionsManager.Instance.CurrentGameOptions.SetByte(ByteOptionNames.MapId, 4); break; + case "fungle": case "thefungle": GameOptionsManager.Instance.CurrentGameOptions.SetByte(ByteOptionNames.MapId, 5); break; diff --git a/Patches/HudSpritePatch.cs b/Patches/HudSpritePatch.cs index 6b10a5c7a..165b7bfa6 100644 --- a/Patches/HudSpritePatch.cs +++ b/Patches/HudSpritePatch.cs @@ -42,7 +42,7 @@ public static class HudSpritePatch if (!Main.EnableCustomButton.Value || !Main.ProcessShapeshifts || Mastermind.ManipulatedPlayers.ContainsKey(player.PlayerId) || ExileController.Instance || GameStates.IsMeeting) return; if ((!SetHudActivePatch.IsActive && !MeetingStates.FirstMeeting) || !player.IsAlive() || Options.CurrentGameMode is not CustomGameMode.Standard and not CustomGameMode.CaptureTheFlag) return; - if (ResetButtonIcons || !AmongUsClient.Instance.IsGameStarted || !Main.IntroDestroyed) + if (ResetButtonIcons || !AmongUsClient.Instance.IsGameStarted || !Main.IntroDestroyed || GameStates.IsLobby || GameStates.IsNotJoined || !GameStates.InGame) { Kill = null; Ability = null; diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index e28e89770..f31c47cef 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -66,7 +66,7 @@ internal static class CheckForEndVotingPatch } } - if (pc.Is(CustomRoles.Dictator) && pva.DidVote && pc.PlayerId != pva.VotedFor && pva.VotedFor < 253 && pc.Data?.IsDead == false) + if (pc.Is(CustomRoles.Dictator) && pva.DidVote && pc.PlayerId != pva.VotedFor && pva.VotedFor < 253 && pc.Data?.IsDead == false && pc.GetTaskState().CompletedTasksCount >= Dictator.MinTasksToDictate.GetInt()) { PlayerControl voteTarget = Utils.GetPlayerById(pva.VotedFor); TryAddAfterMeetingDeathPlayers(PlayerState.DeathReason.Suicide, pc.PlayerId); @@ -234,6 +234,8 @@ internal static class CheckForEndVotingPatch Blackmailer.OnCheckForEndVoting(); NiceSwapper.OnCheckForEndVoting(); + Commited.OnVotingResultsShown(statesList); + states = [.. statesList]; Dictionary votingData = __instance.CustomCalculateVotes(); @@ -1062,6 +1064,7 @@ internal static class MeetingHudStartPatch sb.Append(Witch.GetSpelledMark(target.PlayerId, true)); sb.Append(Wasp.GetStungMark(target.PlayerId)); sb.Append(SpellCaster.HasSpelledMark(seer.PlayerId) ? Utils.ColorString(Team.Coven.GetColor(), "\u25c0") : string.Empty); + sb.Append(Commited.GetMark(seer, target)); if (target.Is(CustomRoles.SuperStar) && Options.EveryOneKnowSuperStar.GetBool()) sb.Append(Utils.ColorString(Utils.GetRoleColor(CustomRoles.SuperStar), "★")); diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index ca9cf5072..2d9927c57 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -314,6 +314,8 @@ internal static class ChangeRoleSettings Camouflage.BlockCamouflage = false; Camouflage.Init(); + Main.NumEmergencyMeetingsUsed = Main.AllPlayerControls.ToDictionary(x => x.PlayerId, _ => 0); + if (AmongUsClient.Instance.AmHost) { string[] invalidColor = Main.AllPlayerControls.Where(p => p.Data.DefaultOutfit.ColorId < 0 || Palette.PlayerColors.Length <= p.Data.DefaultOutfit.ColorId).Select(p => $"{p.name}").ToArray(); @@ -394,6 +396,7 @@ internal static class ChangeRoleSettings Asthmatic.Init(); DoubleShot.Init(); Circumvent.Init(); + Commited.Init(); } catch (Exception ex) { Logger.Exception(ex, "Init Roles"); } @@ -549,6 +552,7 @@ internal static class StartGameHostPatch if (LobbyBehaviour.Instance) { + Main.LobbyBehaviourNetId = LobbyBehaviour.Instance.NetId; MessageWriter writer = MessageWriter.Get(SendOption.Reliable); writer.StartMessage(5); writer.Write(AUClient.GameId); @@ -561,6 +565,11 @@ internal static class StartGameHostPatch AUClient.RemoveNetObject(LobbyBehaviour.Instance); Object.Destroy(LobbyBehaviour.Instance.gameObject); } + else + { + Logger.Fatal($"LobbyBehaviour.Instance is null in {nameof(StartGameHostPatch)}.{nameof(StartGameHost)}", "StartGameHost"); + Main.LobbyBehaviourNetId = uint.MaxValue; + } if (!ShipStatus.Instance) { diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 0a5962290..714aeaa12 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -987,6 +987,17 @@ internal static class ReportDeadBodyPatch // Next, check whether this meeting is allowed //============================================= + if (Main.NumEmergencyMeetingsUsed.TryGetValue(__instance.PlayerId, out int used)) + { + if (used >= Main.RealOptionsData.GetInt(Int32OptionNames.NumEmergencyMeetings)) + { + Notify("NoMoreEmergencyMeetingsLeft"); + return false; + } + + Main.NumEmergencyMeetingsUsed[__instance.PlayerId]++; + } + PlayerControl killer = target?.Object?.GetRealKiller(); CustomRoles? killerRole = killer?.GetCustomRole(); @@ -1149,6 +1160,8 @@ internal static class ReportDeadBodyPatch if (Lovers.PrivateChat.GetBool()) ChatManager.ClearChat(Main.AllAlivePlayerControls.ExceptBy(Main.LoversPlayers.ConvertAll(x => x.PlayerId), x => x.PlayerId).ToArray()); + CustomSabotage.Reset(); + if (target == null) { switch (Main.PlayerStates[player.PlayerId].Role) @@ -1223,6 +1236,7 @@ internal static class ReportDeadBodyPatch Bloodmoon.OnMeetingStart(); Deadlined.OnMeetingStart(); + Commited.OnMeetingStart(); Main.LastVotedPlayerInfo = null; Arsonist.ArsonistTimer.Clear(); diff --git a/Patches/SabotageSystemPatch.cs b/Patches/SabotageSystemPatch.cs index 984285caa..23a357b5b 100644 --- a/Patches/SabotageSystemPatch.cs +++ b/Patches/SabotageSystemPatch.cs @@ -261,7 +261,7 @@ public static class ElectricTaskInitializePatch foreach (PlayerControl pc in Main.AllAlivePlayerControls) { if (pc.GetCustomRole().NeedUpdateOnLights() || pc.Is(CustomRoles.Torch) || pc.Is(CustomRoles.Sleep) || Beacon.IsAffectedPlayer(pc.PlayerId)) - Utils.NotifyRoles(SpecifyTarget: pc, ForceLoop: true); + Utils.NotifyRoles(SpecifyTarget: pc, ForceLoop: true, SendOption: SendOption.None); } } @@ -280,8 +280,6 @@ public static class ElectricTaskCompletePatch if (LastUpdate >= now) return; LastUpdate = now; - Utils.MarkEveryoneDirtySettingsV2(); - if (GameStates.IsInTask) { foreach (PlayerControl pc in Main.AllAlivePlayerControls) @@ -289,16 +287,21 @@ public static class ElectricTaskCompletePatch CustomRoles role = pc.GetCustomRole(); if (role.NeedUpdateOnLights() || pc.Is(CustomRoles.Torch) || pc.Is(CustomRoles.Sleep) || Beacon.IsAffectedPlayer(pc.PlayerId)) - Utils.NotifyRoles(SpecifyTarget: pc, ForceLoop: true); + Utils.NotifyRoles(SpecifyTarget: pc, ForceLoop: true, SendOption: SendOption.None); if (role == CustomRoles.Wiper) { if (Options.UsePhantomBasis.GetBool()) pc.RpcResetAbilityCooldown(); else pc.AddAbilityCD(); } + + if (pc.Is(CustomRoles.Sleep)) + Main.AllPlayerSpeed[pc.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); } } + Utils.MarkEveryoneDirtySettingsV2(); + Logger.Info("Lights sabotage fixed", "ElectricTask"); } } diff --git a/Roles/AddOns/Common/Commited.cs b/Roles/AddOns/Common/Commited.cs new file mode 100644 index 000000000..f1d13efdb --- /dev/null +++ b/Roles/AddOns/Common/Commited.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; + +namespace EHR.AddOns.Common; + +public class Commited : IAddon +{ + public AddonTypes Type => AddonTypes.Helpful; + + private static OptionItem PermanentReduction; + private static OptionItem Reduction; + private static OptionItem IgnoreSkips; + + private static Dictionary Target = []; + public static Dictionary ReduceKCD = []; + + public void SetupCustomOption() + { + Options.SetupAdtRoleOptions(653600, CustomRoles.Commited, canSetNum: true, teamSpawnOptions: true); + + PermanentReduction = new BooleanOptionItem(653610, "Commited.PermanentReduction", false, TabGroup.Addons) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Commited]); + + Reduction = new FloatOptionItem(653611, "Commited.Reduction", new(0.5f, 30f, 0.5f), 5f, TabGroup.Addons) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Commited]) + .SetValueFormat(OptionFormat.Seconds); + + IgnoreSkips = new BooleanOptionItem(653612, "Commited.IgnoreSkips", true, TabGroup.Addons) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Commited]); + } + + public static void Init() + { + Target = []; + ReduceKCD = []; + } + + public static void OnMeetingStart() + { + Target = []; + + if (!PermanentReduction.GetBool()) + ReduceKCD = []; + + foreach (PlayerControl pc in Main.AllAlivePlayerControls) + { + if (pc.Is(CustomRoles.Commited)) + Target[pc.PlayerId] = Main.AllAlivePlayerControls.Where(x => !x.Is(CustomRoles.Commited)).RandomElement().PlayerId; + } + } + + public static void OnVotingResultsShown(List vs) + { + foreach (PlayerControl pc in Main.AllAlivePlayerControls) + { + if (Target.TryGetValue(pc.PlayerId, out byte target) && vs.FindFirst(x => x.VoterId == pc.PlayerId, out MeetingHud.VoterState s) && vs.FindFirst(x => x.VoterId == target, out MeetingHud.VoterState ts) && !(IgnoreSkips.GetBool() && s.SkippedVote) && s.VotedForId == ts.VotedForId) + { + if (ReduceKCD.ContainsKey(pc.PlayerId)) + ReduceKCD[pc.PlayerId] += Reduction.GetFloat(); + else + ReduceKCD[pc.PlayerId] = Reduction.GetFloat(); + } + } + } + + public static string GetMark(PlayerControl seer, PlayerControl target) + { + if (!seer.Is(CustomRoles.Commited) || !Target.TryGetValue(seer.PlayerId, out byte t) || t != target.PlayerId) return string.Empty; + return Utils.ColorString(Utils.GetRoleColor(CustomRoles.Commited), "⌆"); + } +} \ No newline at end of file diff --git a/Roles/Crewmate/Cleanser.cs b/Roles/Crewmate/Cleanser.cs index f0b663a30..f940b4c36 100644 --- a/Roles/Crewmate/Cleanser.cs +++ b/Roles/Crewmate/Cleanser.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using EHR.Modules; +using EHR.Neutral; using Hazel; -using UnityEngine; using static EHR.Options; using static EHR.Translator; @@ -40,11 +40,11 @@ public class Cleanser : RoleBase CancelVote = CreateVoteCancellingUseSetting(Id + 12, CustomRoles.Cleanser, TabGroup.CrewmateRoles); CleanserAbilityUseGainWithEachTaskCompleted = new FloatOptionItem(Id + 13, "AbilityUseGainWithEachTaskCompleted", new(0f, 5f, 0.05f), 0.4f, TabGroup.CrewmateRoles) - .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Cleanser]) + .SetParent(CustomRoleSpawnChances[CustomRoles.Cleanser]) .SetValueFormat(OptionFormat.Times); AbilityChargesWhenFinishedTasks = new FloatOptionItem(Id + 14, "AbilityChargesWhenFinishedTasks", new(0f, 5f, 0.05f), 0.2f, TabGroup.CrewmateRoles) - .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Cleanser]) + .SetParent(CustomRoleSpawnChances[CustomRoles.Cleanser]) .SetValueFormat(OptionFormat.Times); } @@ -95,6 +95,7 @@ public class Cleanser : RoleBase public override bool OnVote(PlayerControl voter, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (DidVote[voter.PlayerId] || Main.DontCancelVoteList.Contains(voter.PlayerId)) return false; DidVote[voter.PlayerId] = true; diff --git a/Roles/Crewmate/Dictator.cs b/Roles/Crewmate/Dictator.cs index 8e761a252..09e95d7fe 100644 --- a/Roles/Crewmate/Dictator.cs +++ b/Roles/Crewmate/Dictator.cs @@ -4,9 +4,14 @@ internal class Dictator : RoleBase { public override bool IsEnable => false; + public static OptionItem MinTasksToDictate; + public override void SetupCustomOption() { Options.SetupRoleOptions(652400, TabGroup.CrewmateRoles, CustomRoles.Dictator); + + MinTasksToDictate = new IntegerOptionItem(652410, "Dictator.MinTasksToDictate", new(0, 10, 1), 3, TabGroup.CrewmateRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Dictator]); } public override void Init() { } diff --git a/Roles/Crewmate/Divinator.cs b/Roles/Crewmate/Divinator.cs index 6ecd4ae76..1110adcd7 100644 --- a/Roles/Crewmate/Divinator.cs +++ b/Roles/Crewmate/Divinator.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using EHR.Modules; +using EHR.Neutral; using Hazel; using static EHR.Options; using static EHR.Translator; @@ -100,6 +101,7 @@ public class Divinator : RoleBase public override bool OnVote(PlayerControl player, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (player == null || target == null) return false; if (DidVote.Contains(player.PlayerId) || Main.DontCancelVoteList.Contains(player.PlayerId)) return false; diff --git a/Roles/Crewmate/Express.cs b/Roles/Crewmate/Express.cs index b22f54d11..fca37094f 100644 --- a/Roles/Crewmate/Express.cs +++ b/Roles/Crewmate/Express.cs @@ -12,13 +12,13 @@ internal class Express : RoleBase public override void SetupCustomOption() { - Options.SetupRoleOptions(5585, TabGroup.CrewmateRoles, CustomRoles.Express); + Options.SetupRoleOptions(653500, TabGroup.CrewmateRoles, CustomRoles.Express); - Options.ExpressSpeed = new FloatOptionItem(5587, "ExpressSpeed", new(0.25f, 5f, 0.25f), 1.5f, TabGroup.CrewmateRoles) + Options.ExpressSpeed = new FloatOptionItem(653502, "ExpressSpeed", new(0.25f, 5f, 0.25f), 1.5f, TabGroup.CrewmateRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Express]) .SetValueFormat(OptionFormat.Multiplier); - Options.ExpressSpeedDur = new IntegerOptionItem(5588, "ExpressSpeedDur", new(0, 90, 1), 5, TabGroup.CrewmateRoles) + Options.ExpressSpeedDur = new IntegerOptionItem(653503, "ExpressSpeedDur", new(0, 90, 1), 5, TabGroup.CrewmateRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Express]) .SetValueFormat(OptionFormat.Seconds); } diff --git a/Roles/Crewmate/Markseeker.cs b/Roles/Crewmate/Markseeker.cs index 91692db02..09c20d634 100644 --- a/Roles/Crewmate/Markseeker.cs +++ b/Roles/Crewmate/Markseeker.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using EHR.Neutral; namespace EHR.Crewmate; @@ -40,6 +41,7 @@ internal class Markseeker : RoleBase public override bool OnVote(PlayerControl player, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (player == null || target == null || player.PlayerId == target.PlayerId || MarkedId != byte.MaxValue || Main.DontCancelVoteList.Contains(player.PlayerId)) return false; MarkedId = target.PlayerId; diff --git a/Roles/Crewmate/Negotiator.cs b/Roles/Crewmate/Negotiator.cs index ba1e2f492..57eabdd0b 100644 --- a/Roles/Crewmate/Negotiator.cs +++ b/Roles/Crewmate/Negotiator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using AmongUs.GameOptions; using EHR.Modules; +using EHR.Neutral; namespace EHR.Crewmate; @@ -17,8 +18,8 @@ public class Negotiator : RoleBase private static OptionItem AbilityUseLimit; private static OptionItem AbilityUseGainWithEachTaskCompleted; private static OptionItem AbilityChargesWhenFinishedTasks; - public static OptionItem CancelVote; + private byte NegotiatorId; private NegotiationType Penalty; private Dictionary> PermanentPenalties; @@ -126,6 +127,7 @@ public class Negotiator : RoleBase public override bool OnVote(PlayerControl voter, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (target == null || voter == null || voter.PlayerId == target.PlayerId || TargetId != byte.MaxValue || voter.GetAbilityUseLimit() < 1f || MinVotingTimeLeftToNegotiate.GetInt() > MeetingTimeManager.VotingTimeLeft || Main.DontCancelVoteList.Contains(voter.PlayerId)) return false; bool votedLast = MeetingHud.Instance.playerStates.All(x => x.TargetPlayerId == voter.PlayerId || x.DidVote); diff --git a/Roles/Crewmate/NiceEraser.cs b/Roles/Crewmate/NiceEraser.cs index eb50d9c0b..611f5e6c6 100644 --- a/Roles/Crewmate/NiceEraser.cs +++ b/Roles/Crewmate/NiceEraser.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using EHR.Neutral; using Hazel; using static EHR.Translator; @@ -60,6 +61,7 @@ internal class NiceEraser : RoleBase public override bool OnVote(PlayerControl player, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (player == null || target == null) return false; if (DidVote.Contains(player.PlayerId) || Main.DontCancelVoteList.Contains(player.PlayerId)) return false; diff --git a/Roles/Crewmate/Oracle.cs b/Roles/Crewmate/Oracle.cs index 53ae063fc..121ab1145 100644 --- a/Roles/Crewmate/Oracle.cs +++ b/Roles/Crewmate/Oracle.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using EHR.Neutral; using Hazel; using static EHR.Options; using static EHR.Translator; @@ -66,6 +67,7 @@ public class Oracle : RoleBase public override bool OnVote(PlayerControl player, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (player == null || target == null) return false; if (DidVote.Contains(player.PlayerId) || Main.DontCancelVoteList.Contains(player.PlayerId)) return false; diff --git a/Roles/Crewmate/President.cs b/Roles/Crewmate/President.cs index a018d45c9..f14e65432 100644 --- a/Roles/Crewmate/President.cs +++ b/Roles/Crewmate/President.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using AmongUs.GameOptions; +using EHR.Neutral; namespace EHR.Crewmate; @@ -234,6 +235,7 @@ public class President : RoleBase public override bool OnVote(PlayerControl voter, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (voter.PlayerId != PresidentId || !IsRecruiting || Main.DontCancelVoteList.Contains(voter.PlayerId)) return false; if (!target.Is(CustomRoleTypes.Crewmate) || target.IsConverted()) diff --git a/Roles/Crewmate/Retributionist.cs b/Roles/Crewmate/Retributionist.cs index 50663185f..2e0bcd27a 100644 --- a/Roles/Crewmate/Retributionist.cs +++ b/Roles/Crewmate/Retributionist.cs @@ -1,5 +1,6 @@ using AmongUs.GameOptions; using EHR.Modules; +using EHR.Neutral; using Hazel; namespace EHR.Crewmate; @@ -12,6 +13,7 @@ public class Retributionist : RoleBase public static OptionItem ResetCampedPlayerAfterEveryMeeting; public static OptionItem UsePet; + public static OptionItem CancelVote; public byte Camping; public bool Notified; @@ -21,7 +23,8 @@ public class Retributionist : RoleBase { StartSetup(653200) .AutoSetupOption(ref ResetCampedPlayerAfterEveryMeeting, false) - .CreatePetUseSetting(ref UsePet); + .CreatePetUseSetting(ref UsePet) + .CreateVoteCancellingUseSetting(ref CancelVote); } public override void Init() @@ -101,6 +104,53 @@ public class Retributionist : RoleBase } } + public override bool OnVote(PlayerControl voter, PlayerControl target) + { + if (!CancelVote.GetBool()) return false; + if (Starspawn.IsDayBreak) return false; + if (target == null || voter == null || voter.PlayerId == target.PlayerId || Main.DontCancelVoteList.Contains(voter.PlayerId)) return false; + + if (!Notified || Camping == byte.MaxValue) return false; + + PlayerControl campTarget = Utils.GetPlayerById(Camping); + if (campTarget == null || campTarget.IsAlive() || !Main.PlayerStates.TryGetValue(campTarget.PlayerId, out PlayerState campState)) return false; + + byte realKiller = campState.GetRealKiller(); + + if (realKiller != target.PlayerId) + { + Notified = false; + Utils.SendMessage("\n", voter.PlayerId, Translator.GetString("Retributionist.Fail")); + } + else + { + PlayerControl killer = Utils.GetPlayerById(realKiller); + + if (killer == null || !killer.IsAlive()) + { + Notified = false; + Utils.SendMessage("\n", voter.PlayerId, Translator.GetString("Retributionist.KillerDead")); + } + else + { + killer.SetRealKiller(voter); + PlayerState killerState = Main.PlayerStates[killer.PlayerId]; + killerState.deathReason = PlayerState.DeathReason.Retribution; + killerState.SetDead(); + Medic.IsDead(killer); + killer.RpcExileV2(); + Utils.AfterPlayerDeathTasks(killer, true); + Utils.SendMessage("\n", title: Utils.ColorString(Utils.GetRoleColor(CustomRoles.Retributionist), string.Format(Translator.GetString("Retributionist.SuccessOthers"), target.PlayerId.ColoredPlayerName(), CustomRoles.Retributionist.ToColoredString()))); + Utils.SendMessage("\n", voter.PlayerId, Translator.GetString("Retributionist.Success")); + } + } + + MeetingManager.SendCommandUsedMessage("/retribute"); + + Main.DontCancelVoteList.Add(voter.PlayerId); + return true; + } + public void ReceiveRPC(MessageReader reader) { Camping = reader.ReadByte(); diff --git a/Roles/Crewmate/Ricochet.cs b/Roles/Crewmate/Ricochet.cs index 7fd84b116..960796986 100644 --- a/Roles/Crewmate/Ricochet.cs +++ b/Roles/Crewmate/Ricochet.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using EHR.Modules; +using EHR.Neutral; using Hazel; namespace EHR.Crewmate; @@ -94,6 +95,7 @@ public class Ricochet : RoleBase public override bool OnVote(PlayerControl pc, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (target == null || pc == null || pc.PlayerId == target.PlayerId || Main.DontCancelVoteList.Contains(pc.PlayerId)) return false; if (pc.GetAbilityUseLimit() >= 1) diff --git a/Roles/Crewmate/Scout.cs b/Roles/Crewmate/Scout.cs index 713ddbd06..caf7a221a 100644 --- a/Roles/Crewmate/Scout.cs +++ b/Roles/Crewmate/Scout.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using EHR.Modules; +using EHR.Neutral; using Hazel; using UnityEngine; using static EHR.Options; @@ -105,6 +106,7 @@ public class Scout : RoleBase public override bool OnVote(PlayerControl player, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (player == null || target == null || player.GetAbilityUseLimit() < 1f || player.PlayerId == target.PlayerId || TrackerTarget[player.PlayerId].Contains(target.PlayerId) || Main.DontCancelVoteList.Contains(player.PlayerId)) return false; player.RpcRemoveAbilityUse(); diff --git a/Roles/Crewmate/Socialite.cs b/Roles/Crewmate/Socialite.cs index 3322e155f..5fd40f5db 100644 --- a/Roles/Crewmate/Socialite.cs +++ b/Roles/Crewmate/Socialite.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using AmongUs.GameOptions; using EHR.Modules; +using EHR.Neutral; using Hazel; namespace EHR.Crewmate; @@ -100,6 +101,7 @@ public class Socialite : RoleBase public override bool OnVote(PlayerControl pc, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (pc == null || target == null || pc.PlayerId == target.PlayerId || Main.DontCancelVoteList.Contains(pc.PlayerId)) return false; if (GuestList.Add(target.PlayerId)) diff --git a/Roles/Crewmate/Soothsayer.cs b/Roles/Crewmate/Soothsayer.cs index 358c7fa6a..b9d05334b 100644 --- a/Roles/Crewmate/Soothsayer.cs +++ b/Roles/Crewmate/Soothsayer.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using EHR.Neutral; namespace EHR.Crewmate; @@ -42,6 +43,7 @@ public class Soothsayer : RoleBase public override bool OnVote(PlayerControl player, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (player == null || target == null || player.PlayerId == target.PlayerId) return false; if (Target != byte.MaxValue || Main.DontCancelVoteList.Contains(player.PlayerId)) return false; diff --git a/Roles/Crewmate/Spiritualist.cs b/Roles/Crewmate/Spiritualist.cs index aab63fc4a..209430887 100644 --- a/Roles/Crewmate/Spiritualist.cs +++ b/Roles/Crewmate/Spiritualist.cs @@ -93,6 +93,8 @@ internal class Spiritualist : RoleBase LastGhostArrowShowTime = 0; ShowGhostArrowUntil = 0; + if (!AmongUsClient.Instance.AmHost) continue; + PlayerControl target = Main.AllPlayerControls.FirstOrDefault(a => a.PlayerId == SpiritualistTarget); if (target == null) continue; diff --git a/Roles/Crewmate/Tether.cs b/Roles/Crewmate/Tether.cs index 878e89c09..f414d7757 100644 --- a/Roles/Crewmate/Tether.cs +++ b/Roles/Crewmate/Tether.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using AmongUs.GameOptions; using EHR.Modules; +using EHR.Neutral; using Hazel; namespace EHR.Crewmate; @@ -116,6 +117,7 @@ public class Tether : RoleBase public override bool OnVote(PlayerControl pc, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (pc == null || target == null || pc.PlayerId == target.PlayerId || Main.DontCancelVoteList.Contains(pc.PlayerId)) return false; if (pc.GetAbilityUseLimit() >= 1) diff --git a/Roles/Crewmate/TimeMaster.cs b/Roles/Crewmate/TimeMaster.cs index 38353f769..4cef94380 100644 --- a/Roles/Crewmate/TimeMaster.cs +++ b/Roles/Crewmate/TimeMaster.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using AmongUs.GameOptions; +using Hazel; using UnityEngine; using static EHR.Options; @@ -12,6 +13,7 @@ internal class TimeMaster : RoleBase { public static bool On; + public static OptionItem TimeMasterCanUseVitals; public static OptionItem TimeMasterRewindTimeLength; public static OptionItem TimeMasterSkillCooldown; public static OptionItem TimeMasterMaxUses; @@ -23,10 +25,15 @@ internal class TimeMaster : RoleBase public override bool IsEnable => On; + private bool DesyncCommsActive; + public override void SetupCustomOption() { SetupRoleOptions(652100, TabGroup.CrewmateRoles, CustomRoles.TimeMaster); + TimeMasterCanUseVitals = new BooleanOptionItem(652109, "TimeMasterCanUseVitals", true, TabGroup.CrewmateRoles) + .SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster]); + TimeMasterRewindTimeLength = new IntegerOptionItem(652110, "TimeMasterRewindTimeLength", new(0, 30, 1), 15, TabGroup.CrewmateRoles) .SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster]) .SetValueFormat(OptionFormat.Seconds); @@ -52,6 +59,7 @@ internal class TimeMaster : RoleBase { On = true; BackTrack = []; + DesyncCommsActive = false; playerId.SetAbilityUseLimit(TimeMasterMaxUses.GetFloat()); } @@ -166,6 +174,51 @@ internal class TimeMaster : RoleBase if (BackTrack.ContainsKey(now)) return; BackTrack[now] = Main.AllAlivePlayerControls.ToDictionary(x => x.PlayerId, x => x.Pos()); + + if (TimeMasterCanUseVitals.GetBool()) return; + + var doComms = false; + bool commsSaboActive = Utils.IsActive(SystemTypes.Comms); + float usableDistance = DisableDevice.UsableDistance + 2f; + Vector2 pos = player.Pos(); + + if (!commsSaboActive) + { + switch (Main.NormalOptions.MapId) + { + case 2: + doComms |= Vector2.Distance(pos, DisableDevice.DevicePos["PolusVital"]) <= usableDistance; + break; + case 4: + doComms |= Vector2.Distance(pos, DisableDevice.DevicePos["AirshipVital"]) <= usableDistance; + break; + case 5: + doComms |= Vector2.Distance(pos, DisableDevice.DevicePos["FungleVital"]) <= usableDistance; + break; + } + } + + var sender = CustomRpcSender.Create("DisableDevice.FixedUpdate", SendOption.Reliable, log: false); + var hasValue = false; + + if (doComms && !player.inVent && !DisableDevice.DesyncComms.Contains(player.PlayerId)) + { + DesyncCommsActive = true; + sender.RpcDesyncRepairSystem(player, SystemTypes.Comms, 128); + hasValue = true; + } + else if (!commsSaboActive && DesyncCommsActive) + { + DesyncCommsActive = false; + sender.RpcDesyncRepairSystem(player, SystemTypes.Comms, 16); + + if (Main.NormalOptions.MapId is 1 or 5) // Mira HQ or The Fungle + sender.RpcDesyncRepairSystem(player, SystemTypes.Comms, 17); + + hasValue = true; + } + + sender.SendMessage(!hasValue); } public override bool CanUseVent(PlayerControl pc, int ventId) diff --git a/Roles/Impostor/BountyHunter.cs b/Roles/Impostor/BountyHunter.cs index 7c752d8fc..54c9cb80d 100644 --- a/Roles/Impostor/BountyHunter.cs +++ b/Roles/Impostor/BountyHunter.cs @@ -214,6 +214,13 @@ public class BountyHunter : RoleBase } } + public override string GetProgressText(byte playerId, bool comms) + { + if (!AmongUsClient.Instance.AmHost) return string.Empty; + if (Timer > 15) return base.GetProgressText(playerId, comms); + return $"{base.GetProgressText(playerId, comms)} <#777777>- {string.Format(GetString("BountyHunterSwapTimer"), Timer)}"; + } + public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) { return GetTargetText(seer, target, hud) + GetTargetArrow(seer, target); @@ -221,9 +228,7 @@ public class BountyHunter : RoleBase private static string GetTargetText(PlayerControl bounty, PlayerControl tar, bool hud) { - if (GameStates.IsMeeting || bounty.PlayerId != tar.PlayerId) return string.Empty; - - if (Main.PlayerStates[bounty.PlayerId].Role is not BountyHunter bh) return string.Empty; + if (GameStates.IsMeeting || bounty.PlayerId != tar.PlayerId || Main.PlayerStates[bounty.PlayerId].Role is not BountyHunter bh) return string.Empty; byte targetId = bh.GetTarget(bounty); return targetId != 0xff ? $"{(hud ? GetString("BountyCurrentTarget") : GetString("Target"))}: {Main.AllPlayerNames[targetId].RemoveHtmlTags().Replace("\r\n", string.Empty)}" : string.Empty; @@ -231,11 +236,7 @@ public class BountyHunter : RoleBase private static string GetTargetArrow(PlayerControl seer, PlayerControl target = null) { - if (target != null && seer.PlayerId != target.PlayerId) return string.Empty; - - if (!ShowTargetArrow || GameStates.IsMeeting) return string.Empty; - - if (Main.PlayerStates[seer.PlayerId].Role is not BountyHunter bh) return string.Empty; + if ((target != null && seer.PlayerId != target.PlayerId) || !ShowTargetArrow || GameStates.IsMeeting || Main.PlayerStates[seer.PlayerId].Role is not BountyHunter bh) return string.Empty; byte targetId = bh.GetTarget(seer); return $" {TargetArrow.GetArrows(seer, targetId)}"; diff --git a/Roles/Impostor/Eraser.cs b/Roles/Impostor/Eraser.cs index b5117600a..f0caa9717 100644 --- a/Roles/Impostor/Eraser.cs +++ b/Roles/Impostor/Eraser.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using EHR.Neutral; using Hazel; using static EHR.Translator; @@ -101,6 +102,7 @@ internal class Eraser : RoleBase public override bool OnVote(PlayerControl player, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (player == null || target == null || EraseMethod.GetInt() == 0) return false; if (DidVote.Contains(player.PlayerId) || Main.DontCancelVoteList.Contains(player.PlayerId)) return false; diff --git a/Roles/Impostor/Godfather.cs b/Roles/Impostor/Godfather.cs index 5c445ecbd..a26739884 100644 --- a/Roles/Impostor/Godfather.cs +++ b/Roles/Impostor/Godfather.cs @@ -1,4 +1,6 @@ -namespace EHR.Impostor; +using EHR.Neutral; + +namespace EHR.Impostor; internal class Godfather : RoleBase { @@ -24,6 +26,7 @@ internal class Godfather : RoleBase public override bool OnVote(PlayerControl voter, PlayerControl target) { + if (Starspawn.IsDayBreak) return false; if (voter == null || target == null || voter.PlayerId == target.PlayerId || Main.DontCancelVoteList.Contains(voter.PlayerId)) return false; GodfatherTarget = target.PlayerId; diff --git a/Roles/Impostor/Vampire.cs b/Roles/Impostor/Vampire.cs index fa1c52e45..e3362803a 100644 --- a/Roles/Impostor/Vampire.cs +++ b/Roles/Impostor/Vampire.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using AmongUs.GameOptions; using EHR.Crewmate; using EHR.Modules; using EHR.Neutral; @@ -75,6 +76,11 @@ public class Vampire : RoleBase PlayerIdList.Remove(playerId); } + public override void ApplyGameOptions(IGameOptions opt, byte playerId) + { + if (IsPoisoner) opt.SetVision(Poisoner.ImpostorVision.GetBool()); + } + public static bool IsVampire(byte playerId) { return PlayerIdList.Contains(playerId); @@ -93,17 +99,11 @@ public class Vampire : RoleBase public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) { if (!IsVampire(killer.PlayerId)) return true; - if (target.Is(CustomRoles.Bait)) return true; - if (target.Is(CustomRoles.Pestilence)) return true; - if (target.Is(CustomRoles.Guardian) && target.AllTasksCompleted()) return true; - if (target.Is(CustomRoles.Opportunist) && target.AllTasksCompleted() && Opportunist.OppoImmuneToAttacksWhenTasksDone.GetBool()) return true; - if (target.Is(CustomRoles.Veteran) && Veteran.VeteranInProtect.ContainsKey(target.PlayerId)) return true; - if (Medic.ProtectList.Contains(target.PlayerId)) return true; if (CanKillNormally) return killer.CheckDoubleTrigger(target, Bite); diff --git a/Roles/Neutral/Bargainer.cs b/Roles/Neutral/Bargainer.cs index ad1448877..69fa5c2d4 100644 --- a/Roles/Neutral/Bargainer.cs +++ b/Roles/Neutral/Bargainer.cs @@ -415,8 +415,8 @@ internal class Bargainer : RoleBase switch (ActiveItems[i].Item) { case Item.LensOfTruth: - if ((AlignmentVisibleOptions)AlignmentVisible.GetValue() is AlignmentVisibleOptions.ForSpecifiedTime or AlignmentVisibleOptions.UntilNextMeeting) indexesToRemove.Add(i); - + if ((AlignmentVisibleOptions)AlignmentVisible.GetValue() is AlignmentVisibleOptions.ForSpecifiedTime or AlignmentVisibleOptions.UntilNextMeeting) + indexesToRemove.Add(i); break; default: indexesToRemove.Add(i); @@ -436,7 +436,6 @@ internal class Bargainer : RoleBase public override bool KnowRole(PlayerControl seer, PlayerControl target) { if (base.KnowRole(seer, target)) return true; - return Main.PlayerStates[seer.PlayerId].Role is Bargainer bg && bg.ActiveItems.Any(x => x.Target == target.PlayerId); } @@ -470,9 +469,7 @@ internal class Bargainer : RoleBase public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) { - if (seer.PlayerId != target.PlayerId) return string.Empty; - - if (Main.PlayerStates[seer.PlayerId].Role is not Bargainer bg) return string.Empty; + if (seer.PlayerId != target.PlayerId || Main.PlayerStates[seer.PlayerId].Role is not Bargainer bg) return string.Empty; var result = string.Empty; diff --git a/Roles/Neutral/Cherokious.cs b/Roles/Neutral/Cherokious.cs index ad6fa21d6..d96ee2efb 100644 --- a/Roles/Neutral/Cherokious.cs +++ b/Roles/Neutral/Cherokious.cs @@ -1,4 +1,5 @@ -using EHR.Crewmate; +using AmongUs.GameOptions; +using EHR.Crewmate; using EHR.Patches; namespace EHR.Neutral; @@ -8,6 +9,7 @@ internal class Cherokious : RoleBase public static bool On; public static OptionItem KillCooldown; + public static OptionItem ImpostorVision; public override bool IsEnable => On; public override void SetupCustomOption() @@ -19,7 +21,10 @@ internal class Cherokious : RoleBase .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Cherokious]) .SetValueFormat(OptionFormat.Seconds); - Options.OverrideTasksData.Create(id + 3, TabGroup.NeutralRoles, CustomRoles.Cherokious); + ImpostorVision = new BooleanOptionItem(id + 3, "ImpostorVision", true, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Cherokious]); + + Options.OverrideTasksData.Create(id + 4, TabGroup.NeutralRoles, CustomRoles.Cherokious); } public override void Add(byte playerId) @@ -33,6 +38,11 @@ internal class Cherokious : RoleBase On = false; } + public override void ApplyGameOptions(IGameOptions opt, byte playerId) + { + opt.SetVision(ImpostorVision.GetBool()); + } + public override void OnTaskComplete(PlayerControl pc, int completedTaskCount, int totalTaskCount) { pc.RpcIncreaseAbilityUseLimitBy(1); diff --git a/Roles/Neutral/Doppelganger.cs b/Roles/Neutral/Doppelganger.cs index 673c8be27..8836be555 100644 --- a/Roles/Neutral/Doppelganger.cs +++ b/Roles/Neutral/Doppelganger.cs @@ -146,6 +146,8 @@ public class Doppelganger : RoleBase private static void RpcChangeSkin(PlayerControl pc, NetworkedPlayerInfo.PlayerOutfit newOutfit) { + if (!AmongUsClient.Instance.AmHost) return; + var sender = CustomRpcSender.Create($"Doppelganger.RpcChangeSkin({pc.Data.PlayerName})", SendOption.Reliable); pc.SetName(newOutfit.PlayerName); diff --git a/Roles/Neutral/Enderman.cs b/Roles/Neutral/Enderman.cs index e49483377..9541c6f33 100644 --- a/Roles/Neutral/Enderman.cs +++ b/Roles/Neutral/Enderman.cs @@ -10,8 +10,9 @@ internal class Enderman : RoleBase private static OptionItem KillCooldown; private static OptionItem CanVent; private static OptionItem Time; - private byte EndermanId = byte.MaxValue; + private static OptionItem ImpostorVision; + private byte EndermanId = byte.MaxValue; private (Vector2 Position, long MarkTimeStamp, bool TP) MarkedPosition = (Vector2.zero, 0, false); private static int Id => 643200; @@ -33,6 +34,9 @@ internal class Enderman : RoleBase Time = new IntegerOptionItem(Id + 4, "EndermanSecondsBeforeTP", new(1, 60, 1), 7, TabGroup.NeutralRoles) .SetParent(CustomRoleSpawnChances[CustomRoles.Enderman]) .SetValueFormat(OptionFormat.Seconds); + + ImpostorVision = new BooleanOptionItem(Id + 5, "ImpostorVision", true, TabGroup.NeutralRoles) + .SetParent(CustomRoleSpawnChances[CustomRoles.Enderman]); } public override void Init() @@ -64,7 +68,7 @@ internal class Enderman : RoleBase public override void ApplyGameOptions(IGameOptions opt, byte id) { - opt.SetVision(true); + opt.SetVision(ImpostorVision.GetBool()); if (UsePhantomBasis.GetBool() && UsePhantomBasisForNKs.GetBool()) AURoleOptions.PhantomCooldown = Time.GetInt() + 2f; } diff --git a/Roles/Neutral/Executioner.cs b/Roles/Neutral/Executioner.cs index 6868d0432..39b70369a 100644 --- a/Roles/Neutral/Executioner.cs +++ b/Roles/Neutral/Executioner.cs @@ -68,7 +68,7 @@ public class Executioner : RoleBase { List targetList = []; targetList.AddRange(from target in Main.AllPlayerControls where playerId != target.PlayerId where CanTargetImpostor.GetBool() || !target.Is(CustomRoleTypes.Impostor) where CanTargetNeutralKiller.GetBool() || !target.IsNeutralKiller() where CanTargetNeutralBenign.GetBool() || !target.IsNeutralBenign() where CanTargetNeutralEvil.GetBool() || (!target.IsNeutralEvil() && !target.IsNeutralPariah()) where CanTargetCoven.GetBool() || !target.Is(CustomRoleTypes.Coven) where target.GetCustomRole() is not (CustomRoles.GM or CustomRoles.SuperStar) where Main.LoversPlayers.TrueForAll(x => x.PlayerId != playerId) select target); - targetList.AddRange(Main.AllPlayerControls.Where(x => x.IsCrewmate())); + targetList.AddRange(Main.AllPlayerControls.Where(x => x.IsCrewmate() && x.GetCustomRole() is not (CustomRoles.NiceSwapper or CustomRoles.Mayor or CustomRoles.NiceGuesser or CustomRoles.Dictator))); if (!CanTargetNeutralBenign.GetBool() && !CanTargetNeutralEvil.GetBool() && !CanTargetNeutralKiller.GetBool()) targetList.RemoveAll(x => x.GetCustomRole().IsNeutral() || x.Is(CustomRoles.Bloodlust)); if (targetList.Count == 0) diff --git a/Roles/Neutral/Lawyer.cs b/Roles/Neutral/Lawyer.cs index f22b64710..6c3497985 100644 --- a/Roles/Neutral/Lawyer.cs +++ b/Roles/Neutral/Lawyer.cs @@ -92,6 +92,7 @@ public class Lawyer : RoleBase public override void Remove(byte playerId) { PlayerIdList.Remove(playerId); + Target.Remove(playerId); } public static void SendRPC(byte lawyerId, byte targetId = 0x73, string Progress = "") diff --git a/Roles/Neutral/Nonplus.cs b/Roles/Neutral/Nonplus.cs index d9cf3d4e9..0ee8be183 100644 --- a/Roles/Neutral/Nonplus.cs +++ b/Roles/Neutral/Nonplus.cs @@ -1,4 +1,6 @@ -namespace EHR.Neutral; +using AmongUs.GameOptions; + +namespace EHR.Neutral; public class Nonplus : RoleBase { @@ -8,6 +10,7 @@ public class Nonplus : RoleBase public static OptionItem BlindDuration; public static OptionItem UseLimit; public static OptionItem NonplusAbilityUseGainWithEachKill; + public static OptionItem ImpostorVision; public override bool IsEnable => On; public override void SetupCustomOption() @@ -33,6 +36,9 @@ public class Nonplus : RoleBase NonplusAbilityUseGainWithEachKill = new FloatOptionItem(id + 5, "AbilityUseGainWithEachKill", new(0f, 5f, 0.1f), 1.5f, tab) .SetParent(Options.CustomRoleSpawnChances[role]) .SetValueFormat(OptionFormat.Times); + + ImpostorVision = new BooleanOptionItem(id + 6, "ImpostorVision", true, tab) + .SetParent(Options.CustomRoleSpawnChances[role]); } public override void Init() @@ -46,6 +52,11 @@ public class Nonplus : RoleBase playerId.SetAbilityUseLimit(UseLimit.GetFloat()); } + public override void ApplyGameOptions(IGameOptions opt, byte playerId) + { + opt.SetVision(ImpostorVision.GetBool()); + } + public override bool CanUseImpostorVentButton(PlayerControl pc) { return true; diff --git a/Roles/Neutral/Phantasm.cs b/Roles/Neutral/Phantasm.cs index 9e497f7ea..bc147bc6e 100644 --- a/Roles/Neutral/Phantasm.cs +++ b/Roles/Neutral/Phantasm.cs @@ -25,4 +25,13 @@ internal class Phantasm : RoleBase public override void Init() { } public override void Add(byte playerId) { } + + public override void OnTaskComplete(PlayerControl pc, int completedTaskCount, int totalTaskCount) + { + if (completedTaskCount + 1 >= totalTaskCount) + { + CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Phantom); + CustomWinnerHolder.WinnerIds.Add(pc.PlayerId); + } + } } \ No newline at end of file diff --git a/Roles/Neutral/PlagueBearer.cs b/Roles/Neutral/PlagueBearer.cs index be1eac394..7f4edf530 100644 --- a/Roles/Neutral/PlagueBearer.cs +++ b/Roles/Neutral/PlagueBearer.cs @@ -125,6 +125,7 @@ public class PlagueBearer : RoleBase } PlaguedList[killer.PlayerId].Add(target.PlayerId); + SendRPC(killer, target); Utils.NotifyRoles(SpecifySeer: killer, SpecifyTarget: target); killer.ResetKillCooldown(); killer.SetKillCooldown(); diff --git a/Roles/Neutral/Poisoner.cs b/Roles/Neutral/Poisoner.cs index 9f49f9bc6..0dd52fee4 100644 --- a/Roles/Neutral/Poisoner.cs +++ b/Roles/Neutral/Poisoner.cs @@ -7,6 +7,7 @@ public class Poisoner : RoleBase public static OptionItem CanVent; public static OptionItem KillCooldown; public static OptionItem CanKillNormally; + public static OptionItem ImpostorVision; public override bool IsEnable => false; @@ -27,6 +28,9 @@ public class Poisoner : RoleBase CanKillNormally = new BooleanOptionItem(Id + 13, "CanKillNormally", true, TabGroup.NeutralRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Poisoner]); + + ImpostorVision = new BooleanOptionItem(Id + 14, "ImpostorVision", true, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Poisoner]); } public override void Init() { } diff --git a/Roles/Neutral/Rogue.cs b/Roles/Neutral/Rogue.cs index 70bbb39ff..cee572082 100644 --- a/Roles/Neutral/Rogue.cs +++ b/Roles/Neutral/Rogue.cs @@ -17,8 +17,8 @@ public class Rogue : RoleBase private static OptionItem KillCooldown; private static OptionItem CanVent; - private bool AllTasksCompleted; + private bool AllTasksCompleted; private int Count; private (Objective Objective, Reward Reward, object Data, bool IsCompleted) CurrentTask; private bool DoCheck; @@ -93,18 +93,29 @@ public class Rogue : RoleBase public override bool OnSabotage(PlayerControl pc) { - if (GotRewards.Contains(Reward.Morph) && MorphCooldown <= 0) + if (!Options.UsePets.GetBool() && GotRewards.Contains(Reward.Morph) && MorphCooldown <= 0) { - MorphCooldown = 15 + (int)Options.AdjustedDefaultKillCooldown; - Utils.SendRPC(CustomRPC.SyncRoleData, pc.PlayerId, 2, MorphCooldown); - PlayerControl target = Main.AllAlivePlayerControls.Except([pc]).RandomElement(); - pc.RpcShapeshift(target, !Options.DisableAllShapeshiftAnimations.GetBool()); + Morph(pc); return false; } return GotRewards.Contains(Reward.Sabotage) || pc.Is(CustomRoles.Mischievous); } + public override void OnPet(PlayerControl pc) + { + if (GotRewards.Contains(Reward.Morph) && MorphCooldown <= 0) + Morph(pc); + } + + private void Morph(PlayerControl pc) + { + MorphCooldown = 15 + (int)Options.AdjustedDefaultKillCooldown; + Utils.SendRPC(CustomRPC.SyncRoleData, pc.PlayerId, 2, MorphCooldown); + PlayerControl target = Main.AllAlivePlayerControls.Except([pc]).RandomElement(); + pc.RpcShapeshift(target, !Options.DisableAllShapeshiftAnimations.GetBool()); + } + public override bool OnCheckMurderAsTarget(PlayerControl killer, PlayerControl target) { return !GotRewards.Contains(Reward.Shield); diff --git a/Roles/Neutral/Tiger.cs b/Roles/Neutral/Tiger.cs index b33f3e88a..1d30cb3e8 100644 --- a/Roles/Neutral/Tiger.cs +++ b/Roles/Neutral/Tiger.cs @@ -14,6 +14,7 @@ internal class Tiger : RoleBase public static OptionItem EnrageDuration; public static OptionItem KillCooldown; public static OptionItem CanVent; + public static OptionItem ImpostorVision; public static bool On; @@ -45,6 +46,9 @@ internal class Tiger : RoleBase CanVent = new BooleanOptionItem(Id + 6, "CanVent", true, TabGroup.NeutralRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Tiger]); + + ImpostorVision = new BooleanOptionItem(Id + 7, "ImpostorVision", true, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Tiger]); } public override void Add(byte playerId) @@ -76,7 +80,10 @@ internal class Tiger : RoleBase public override void ApplyGameOptions(IGameOptions opt, byte playerId) { - if (Options.UsePhantomBasis.GetBool() && Options.UsePhantomBasisForNKs.GetBool()) AURoleOptions.PhantomCooldown = EnrageCooldown.GetFloat() + EnrageDuration.GetFloat(); + opt.SetVision(ImpostorVision.GetBool()); + + if (Options.UsePhantomBasis.GetBool() && Options.UsePhantomBasisForNKs.GetBool()) + AURoleOptions.PhantomCooldown = EnrageCooldown.GetFloat() + EnrageDuration.GetFloat(); } public override bool OnSabotage(PlayerControl pc)