using System; using System.Collections.Generic; using System.Linq; using AmongUs.GameOptions; using EHR.Modules; using EHR.Roles; using HarmonyLib; using Hazel; using UnityEngine; namespace EHR.Gamemodes; internal static class CustomHnS { public static int TimeLeft; private static long LastUpdate; public static bool IsBlindTime; private static OptionItem MaxGameLength; private static OptionItem MinNeutrals; private static OptionItem MaxNeutrals; private static OptionItem DangerMeter; private static OptionItem PlayersSeeRoles; private static OptionItem ChatDuringGame; private static OptionItem ShiftAndSeek; public static Dictionary> HideAndSeekRoles = []; public static Dictionary PlayerRoles = []; public static Dictionary Danger = []; public static List AllHnSRoles = []; public static int SeekerNum => Math.Max(Main.RealOptionsData.GetInt(Int32OptionNames.NumImpostors), 1); public static int RandomNeutralsNum => IRandom.Instance.Next(MinNeutrals.GetInt(), MaxNeutrals.GetInt() + 1); public static int MaximumGameLength => MaxGameLength.GetInt(); public static bool Chat => ChatDuringGame.GetBool(); public static bool SNS { get => ShiftAndSeek.GetBool(); set => ShiftAndSeek.SetValue(value ? 1 : 0); } public static void SetupCustomOption() { const int id = 69_211_001; Color color = new(52, 94, 235, byte.MaxValue); MaxGameLength = new IntegerOptionItem(id, "FFA_GameTime", new(0, 1200, 10), 600, TabGroup.GameSettings) .SetGameMode(CustomGameMode.HideAndSeek) .SetValueFormat(OptionFormat.Seconds) .SetColor(color); MinNeutrals = new IntegerOptionItem(id + 1, "HNS.MinNeutrals", new(0, 13, 1), 0, TabGroup.GameSettings) .SetGameMode(CustomGameMode.HideAndSeek) .SetColor(color); MaxNeutrals = new IntegerOptionItem(id + 2, "HNS.MaxNeutrals", new(0, 13, 1), 2, TabGroup.GameSettings) .SetGameMode(CustomGameMode.HideAndSeek) .SetColor(color); DangerMeter = new BooleanOptionItem(id + 3, "HNS.DangerMeter", true, TabGroup.GameSettings) .SetGameMode(CustomGameMode.HideAndSeek) .SetColor(color); PlayersSeeRoles = new BooleanOptionItem(id + 4, "HNS.PlayersSeeRoles", true, TabGroup.GameSettings) .SetGameMode(CustomGameMode.HideAndSeek) .SetColor(color); ChatDuringGame = new BooleanOptionItem(id + 5, "FFA_ChatDuringGame", false, TabGroup.GameSettings) .SetGameMode(CustomGameMode.HideAndSeek) .SetColor(color); ShiftAndSeek = new BooleanOptionItem(id + 6, "HNS.ShiftAndSeek", false, TabGroup.GameSettings) .SetGameMode(CustomGameMode.HideAndSeek) .SetColor(color) .RegisterUpdateValueEvent((_, _, newValue) => Options.CustomRoleSpawnChances[CustomRoles.Disguiser].SetValue(newValue * (100 / 5))) .SetRunEventOnLoad(true); } public static void Init() { TimeLeft = MaxGameLength.GetInt(); LastUpdate = Utils.TimeStamp; Type[] types = GetAllHnsRoleTypes(); AllHnSRoles = GetAllHnsRoles(types); bool sns = ShiftAndSeek.GetBool(); HideAndSeekRoles = types .Select(x => (IHideAndSeekRole)Activator.CreateInstance(x)) .Where(x => x != null) .Join(AllHnSRoles, x => x.GetType().Name.ToLower(), x => x.ToString().ToLower(), (Interface, Enum) => (Enum, Interface)) .Where(x => !sns ? x.Enum is CustomRoles.Seeker or CustomRoles.Hider || x.Enum.GetMode() != 0 : x.Enum == CustomRoles.Disguiser || x.Interface.Team != Team.Impostor && x.Enum.GetMode() != 0) .Where(x => (!x.Enum.OnlySpawnsWithPets() || Options.UsePets.GetBool()) && (x.Enum != CustomRoles.Agent || SeekerNum >= 2) && x.Interface.Count > 0 && (x.Interface.Team == Team.Neutral || x.Interface.Chance > IRandom.Instance.Next(100))) .OrderBy(x => x.Enum is CustomRoles.Seeker or CustomRoles.Hider ? 100 : IRandom.Instance.Next(100)) .GroupBy(x => x.Interface.Team) .ToDictionary(x => x.Key, x => x.ToDictionary(y => y.Enum, y => y.Interface.Count)); PlayerRoles = []; } public static void StartSeekerBlindTime() { Main.AllPlayerKillCooldown.SetAllValues(Seeker.KillCooldown.GetFloat()); IsBlindTime = true; Utils.MarkEveryoneDirtySettingsV4(); LateTask.New(() => { IsBlindTime = false; Utils.MarkEveryoneDirtySettingsV4(); Main.EnumerateAlivePlayerControls() .Join(PlayerRoles, x => x.PlayerId, x => x.Key, (pc, role) => (pc, role.Value.Interface)) .Where(x => x.Interface.Team == Team.Impostor) .Do(x => x.pc.SetKillCooldown()); LateTask.New(() => Main.Instance.StartCoroutine(Utils.NotifyEveryoneAsync(noCache: false)), 3f, log: false); }, Seeker.BlindTime.GetFloat() + 14f, "Blind Time Expire"); } public static List GetAllHnsRoles(Type[] types) { List roles = []; foreach (Type type in types) { if (Enum.TryParse(type.Name, ignoreCase: true, out CustomRoles role)) roles.Add(role); } return roles; } private static Type[] CachedHnsTypes; public static Type[] GetAllHnsRoleTypes() { return CachedHnsTypes ??= Main.AllTypes .Where(t => typeof(IHideAndSeekRole).IsAssignableFrom(t) && !t.IsInterface) .ToArray(); } public static void AssignRoles() { Dictionary result = []; List allPlayers = [.. Main.EnumeratePlayerControls()]; if (Main.GM.Value) allPlayers.RemoveAll(x => x.AmOwner); allPlayers.RemoveAll(x => ChatCommands.Spectators.Contains(x.PlayerId)); allPlayers.Shuffle(); Dictionary memberNum = new() { [Team.Neutral] = RandomNeutralsNum, [Team.Impostor] = SeekerNum }; memberNum[Team.Crewmate] = allPlayers.Count - memberNum.Values.Sum(); Logger.Warn($"Number of impostors: {memberNum[Team.Impostor]}", "HnsRoleAssigner"); Dictionary preSetRoles = Main.SetRoles.AddRange(ChatCommands.DraftResult, false); if (ChatCommands.DraftResult.Count > 0 && ChatCommands.DraftResult.Count + preSetRoles.Count >= allPlayers.Count && preSetRoles.All(x => x.Value is not (CustomRoles.Seeker or CustomRoles.Locator or CustomRoles.Dasher or CustomRoles.Venter or CustomRoles.Agent or CustomRoles.Disguiser))) { byte removeKey = ChatCommands.DraftResult.Keys.RandomElement(); ChatCommands.DraftResult.Remove(removeKey); preSetRoles.Remove(removeKey); } foreach (KeyValuePair item in preSetRoles) { try { PlayerControl pc = allPlayers.FirstOrDefault(x => x.PlayerId == item.Key); if (!pc) continue; result[pc] = item.Value; allPlayers.RemoveAll(x => x.PlayerId == item.Key); KeyValuePair> role = HideAndSeekRoles.FirstOrDefault(x => x.Value.ContainsKey(item.Value)); role.Value[item.Value]--; memberNum[role.Key]--; Logger.Warn($"Pre-Set Role Assigned: {pc.GetRealName()} => {item.Value}", "HnsRoleAssigner"); } catch (Exception e) { Utils.ThrowException(e); } } Dictionary playerTeams = Main.TeamValues[1..4] .SelectMany(x => Enumerable.Repeat(x, Math.Max(memberNum[x], 0))) .Shuffle() .Zip(allPlayers) .GroupBy(x => x.First, x => x.Second) .ToDictionary(x => x.Key, x => x.ToArray()); if (memberNum[Team.Neutral] > 0 && HideAndSeekRoles.TryGetValue(Team.Neutral, out Dictionary neutrals)) HideAndSeekRoles[Team.Neutral] = neutrals.Shuffle().ToDictionary(x => x.Key, x => x.Value); foreach ((Team team, Dictionary roleCounts) in HideAndSeekRoles) { try { if (playerTeams[team].Length == 0 || memberNum[team] <= 0) continue; } catch (KeyNotFoundException) { continue; } foreach ((CustomRoles role, int count) in roleCounts) { for (var i = 0; i < count; i++) { try { PlayerControl pc = playerTeams[team][0]; if (!pc) continue; result[pc] = role; allPlayers.Remove(pc); playerTeams[team] = playerTeams[team][1..]; memberNum[team]--; if (memberNum[team] <= 0) break; } catch (Exception e) { if (e is IndexOutOfRangeException) break; Utils.ThrowException(e); } } if (playerTeams[team].Length == 0 || memberNum[team] <= 0) break; } } foreach (PlayerControl pc in allPlayers.Except(result.Keys).ToArray()) { Logger.Warn($"Unassigned, force Hider: {pc.GetRealName()} => {CustomRoles.Hider}", "HnsRoleAssigner"); result[pc] = CustomRoles.Hider; memberNum[Team.Crewmate]--; allPlayers.Remove(pc); } if (allPlayers.Count > 0) Logger.Error($"Some players were not assigned a role: {allPlayers.Join(x => x.GetRealName())}", "HnsRoleAssigner"); Logger.Msg($"Roles: {result.Join(x => $"{x.Key.GetRealName()} => {x.Value}")}", "HnsRoleAssigner"); Dictionary roleInterfaces = Main.AllTypes .Where(x => typeof(IHideAndSeekRole).IsAssignableFrom(x) && !x.IsInterface) .Select(x => (IHideAndSeekRole)Activator.CreateInstance(x)) .Where(x => x != null) .ToDictionary(x => x.GetType().Name, x => x); PlayerRoles = result.ToDictionary(x => x.Key.PlayerId, x => (roleInterfaces.GetValueOrDefault(x.Value.ToString(), new Hider()), x.Value)); if (Main.GM.Value) { PlayerControl lp = PlayerControl.LocalPlayer; result[lp] = CustomRoles.GM; PlayerRoles[lp.PlayerId] = (new Hider(), CustomRoles.GM); } foreach (byte spectator in ChatCommands.Spectators) PlayerRoles[spectator] = (new Hider(), CustomRoles.GM); LateTask.New(() => result.IntersectBy(Main.PlayerStates.Keys, x => x.Key.PlayerId).Do(x => x.Key.RpcSetCustomRole(x.Value)), 5f, log: false); // ================================================================================================================== if (result.ContainsValue(CustomRoles.Agent)) { byte agent = result.GetKeyByValue(CustomRoles.Agent).PlayerId; PlayerRoles.DoIf(x => x.Value.Role != CustomRoles.Agent && x.Value.Interface.Team == Team.Impostor, x => TargetArrow.Add(x.Key, agent)); } if (!Utils.DoRPC) return; MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.HNSSync, SendOption.Reliable); writer.WritePacked(1); writer.WritePacked(TimeLeft); writer.WritePacked(Danger.Count); foreach (var kv in Danger) { writer.Write(kv.Key); writer.WritePacked(kv.Value); } writer.WritePacked(PlayerRoles.Count); foreach (var kv in PlayerRoles) { writer.Write(kv.Key); writer.WritePacked((int)kv.Value.Role); } AmongUsClient.Instance.FinishRpcImmediately(writer); } public static void ApplyGameOptions(IGameOptions opt, PlayerControl pc) { (IHideAndSeekRole Interface, CustomRoles Role) role = PlayerRoles.GetValueOrDefault(pc.PlayerId); bool blind = role.Interface.Team == Team.Impostor && IsBlindTime; Main.AllPlayerSpeed[pc.PlayerId] = blind ? Main.MinSpeed : role.Interface.RoleSpeed; opt.SetFloat(FloatOptionNames.CrewLightMod, blind ? 0f : role.Interface.RoleVision); opt.SetFloat(FloatOptionNames.ImpostorLightMod, blind ? 0f : role.Interface.RoleVision); opt.SetFloat(FloatOptionNames.PlayerSpeedMod, Main.AllPlayerSpeed[pc.PlayerId]); } public static bool KnowTargetRoleColor(PlayerControl seer, PlayerControl target, ref string color) { if (seer.PlayerId == target.PlayerId) return true; if (!PlayerRoles.TryGetValue(target.PlayerId, out (IHideAndSeekRole Interface, CustomRoles Role) targetRole)) return false; if (!PlayerRoles.TryGetValue(seer.PlayerId, out (IHideAndSeekRole Interface, CustomRoles Role) seerRole)) return false; if (PlayersSeeRoles.GetBool()) { color = Utils.GetRoleColorCode(targetRole.Role); if (targetRole.Role == CustomRoles.Agent) color = Utils.GetRoleColorCode(CustomRoles.Hider); return true; } if (targetRole.Interface.Team == Team.Impostor && (targetRole.Role != CustomRoles.Agent || seerRole.Interface.Team == Team.Impostor)) { color = Utils.GetRoleColorCode(CustomRoles.Seeker); return true; } return false; } public static bool HasTasks(NetworkedPlayerInfo playerInfo) { if (!AmongUsClient.Instance.AmHost && playerInfo.Object.AmOwner) return PlayerControl.LocalPlayer.GetCustomRole() is CustomRoles.Taskinator or CustomRoles.Hider or CustomRoles.Jet or CustomRoles.Detector or CustomRoles.Jumper; if (!PlayerRoles.TryGetValue(playerInfo.PlayerId, out (IHideAndSeekRole Interface, CustomRoles Role) role)) return false; return (role.Interface.Team == Team.Crewmate || role.Role == CustomRoles.Taskinator) && role.Role != CustomRoles.GM; } public static bool IsRoleTextEnabled(PlayerControl seer, PlayerControl target) { if (seer.PlayerId == target.PlayerId || PlayersSeeRoles.GetBool()) return true; if (!PlayerRoles.TryGetValue(target.PlayerId, out (IHideAndSeekRole Interface, CustomRoles Role) targetRole)) return false; if (!PlayerRoles.TryGetValue(seer.PlayerId, out (IHideAndSeekRole Interface, CustomRoles Role) seerRole)) return false; return targetRole.Interface.Team == Team.Impostor && (targetRole.Role != CustomRoles.Agent || seerRole.Interface.Team == Team.Impostor); } private static readonly StringBuilder TaskBarText = new(); public static string GetTaskBarText() { bool seeRoles = PlayersSeeRoles.GetBool(); bool agentEnabled = CustomRoles.Agent.IsEnable(); string deadText = Translator.GetString("Dead"); TaskBarText.Clear(); TaskBarText.Append(""); foreach ((byte id, PlayerState state) in Main.PlayerStates) { if (!PlayerRoles.TryGetValue(id, out var playerRole)) continue; string name = Main.AllPlayerNames.TryGetValue(id, out string playerName) ? playerName : $"ID {id}"; name = Utils.ColorString(Main.PlayerColors.GetValueOrDefault(id, Color.white), name); TaskBarText.Append(name); if (seeRoles) { CustomRoles role = state.MainRole == CustomRoles.Agent ? CustomRoles.Hider : state.MainRole; TaskBarText.Append(' '); TaskBarText.Append('('); TaskBarText.Append(role.ToColoredString()); TaskBarText.Append(')'); TaskState taskState = state.TaskState; if (state.MainRole != CustomRoles.GM && !agentEnabled && taskState.HasTasks) { TaskBarText.Append(' '); TaskBarText.Append('('); TaskBarText.Append(taskState.CompletedTasksCount); TaskBarText.Append('/'); TaskBarText.Append(taskState.AllTasksCount); TaskBarText.Append(')'); } } else if (playerRole.Interface.Team == Team.Impostor) { TaskBarText.Append(' '); TaskBarText.Append('('); TaskBarText.Append(CustomRoles.Seeker.ToColoredString()); TaskBarText.Append(')'); } if (state.IsDead) { TaskBarText.Append(" "); TaskBarText.Append(deadText); TaskBarText.Append(""); } TaskBarText.Append('\n'); } TaskBarText.Append("\r\n\r\n<#00ffa5>"); TaskBarText.Append(Translator.GetString("HNS.TaskCount")); TaskBarText.Append(" "); TaskBarText.Append(GameData.Instance.CompletedTasks); TaskBarText.Append('/'); TaskBarText.Append(GameData.Instance.TotalTasks); return TaskBarText.ToString(); } private static readonly StringBuilder Suffix = new(); public static string GetSuffixText(PlayerControl seer, PlayerControl target, bool hud = false) { if (!Main.IntroDestroyed || seer.PlayerId != target.PlayerId || seer.IsModdedClient() && !hud || TimeLeft < 0) return string.Empty; Suffix.Clear(); if (PlayerRoles.TryGetValue(seer.PlayerId, out (IHideAndSeekRole Interface, CustomRoles Role) seerRole)) { if (seerRole.Interface.Team == Team.Impostor) { if (PlayerRoles.FindFirst(x => x.Value.Role == CustomRoles.Agent, out KeyValuePair kvp)) { byte agent = kvp.Key; Suffix.Append(TargetArrow.GetArrows(seer, agent)); } } else { Suffix.Append(GetDangerMeter(seer)); if (PlayersSeeRoles.GetBool() && !CustomRoles.Agent.IsEnable() && IntroCutsceneDestroyPatch.IntroDestroyTS + 15 > Utils.TimeStamp) { Suffix.AppendLine(); Suffix.AppendFormat(Translator.GetString("HNSSeekers"), PlayerRoles.Where(x => x.Value.Interface.Team == Team.Impostor).Join(", ", x => x.Key.ColoredPlayerName())); } } } Suffix.AppendLine(); if (TimeLeft <= 60) { Suffix.Append("'); Suffix.Append(Translator.GetString("TimeLeft")); Suffix.Append(": "); Suffix.Append(TimeLeft); Suffix.Append('s'); } else { int minutes = TimeLeft / 60; int seconds = TimeLeft % 60; if (hud) { Suffix.Append($"{minutes:00}"); Suffix.Append(':'); Suffix.Append($"{seconds:00}"); } else { Suffix.AppendFormat(Translator.GetString("MinutesLeft"), $"{minutes}-{minutes + 1}"); } } return Suffix.ToString(); } private static string GetDangerMeter(PlayerControl seer) { return Danger.TryGetValue(seer.PlayerId, out int danger) ? danger <= 5 ? $"\n{new string('\u25a0', 5 - danger)}{new string('\u25a1', danger)}" : $"\n{new string('\u25a1', 5)}" : string.Empty; string GetColorFromDanger() // 0: Highest, 4: Lowest => danger switch { 0 => "#ff1313", 1 => "#ff6a00", 2 => "#ffaa00", 3 => "#ffea00", 4 => "#ffff00", _ => "#ffffff" }; } public static string GetRoleInfoText(PlayerControl seer) { return $"{Utils.ColorString(Utils.GetRoleColor(seer.GetCustomRole()), seer.GetRoleInfo())}"; } public static void ReceiveRPC(MessageReader reader) { switch (reader.ReadPackedInt32()) { case 1: { TimeLeft = reader.ReadPackedInt32(); int dangerCount = reader.ReadPackedInt32(); Danger.Clear(); for (int i = 0; i < dangerCount; i++) { byte id = reader.ReadByte(); int danger = reader.ReadPackedInt32(); Danger[id] = danger; } int roleCount = reader.ReadPackedInt32(); PlayerRoles.Clear(); for (int i = 0; i < roleCount; i++) { byte id = reader.ReadByte(); CustomRoles role = (CustomRoles)reader.ReadPackedInt32(); var roleInterface = role == CustomRoles.GM ? new Hider() : (IHideAndSeekRole)Activator.CreateInstance(Main.AllTypes.First(t => t.Name == role.ToString())); PlayerRoles[id] = (roleInterface, role); } break; } case 2: { PlayerRoles.Remove(reader.ReadByte()); break; } case 3: { TimeLeft = reader.ReadPackedInt32(); Danger.Clear(); while (reader.BytesRemaining > 0) Danger[reader.ReadByte()] = reader.ReadPackedInt32(); break; } } } public static bool CheckForGameEnd(out GameOverReason reason) { reason = GameOverReason.ImpostorsByKill; var alivePlayers = Main.CachedAlivePlayerControls(); // If there are 0 players alive, the game is over and only foxes win if (alivePlayers.Count == 0) { reason = GameOverReason.CrewmateDisconnect; CustomWinnerHolder.ResetAndSetWinner(CustomWinner.None); AddFoxesToWinners(); return true; } // If there are no crew roles left, the game is over and only impostors win if (alivePlayers.All(x => PlayerRoles.TryGetValue(x.PlayerId, out var role) && role.Interface.Team != Team.Crewmate)) { reason = GameOverReason.HideAndSeek_ImpostorsByKills; SetWinners(CustomWinner.Seeker, Team.Impostor); return true; } // If time is up or there are no impostors in the game, the game is over and crewmates win if (TimeLeft <= 0 || PlayerRoles.Values.All(x => x.Interface.Team != Team.Impostor)) { reason = TimeLeft <= 0 ? GameOverReason.HideAndSeek_CrewmatesByTimer : GameOverReason.ImpostorDisconnect; SetWinners(CustomWinner.Hider, Team.Crewmate); return true; } return false; static void SetWinners(CustomWinner winner, Team team) { CustomWinnerHolder.ResetAndSetWinner(winner); CustomWinnerHolder.WinnerIds.UnionWith(PlayerRoles.Where(x => x.Value.Interface.Team == team).Select(x => x.Key)); AddFoxesToWinners(); } } public static void AddFoxesToWinners() { List foxes = Main.PlayerStates.Values.Where(x => x.MainRole == CustomRoles.Fox && x.Player && x.Player.IsAlive()).Select(x => x.Player.PlayerId).ToList(); if (foxes.Count == 0) return; CustomWinnerHolder.AdditionalWinnerTeams.Add(AdditionalWinners.Fox); CustomWinnerHolder.WinnerIds.UnionWith(foxes); } public static void OnCheckMurder(PlayerControl killer, PlayerControl target) { if (PlayerRoles[killer.PlayerId].Interface.Team != Team.Impostor || PlayerRoles[target.PlayerId].Interface.Team == Team.Impostor || IsBlindTime) return; if (killer.Is(CustomRoles.Disguiser) && !Disguiser.CheckMurder(killer, target)) return; killer.Kill(target); if (Main.GM.Value && AmongUsClient.Instance.AmHost) PlayerControl.LocalPlayer.KillFlash(); ChatCommands.Spectators.ToValidPlayers().Do(x => x.KillFlash()); // If the Troll is killed, they win if (target.Is(CustomRoles.Troll)) { CustomSoundsManager.RPCPlayCustomSoundAll("Congrats"); CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Troll); CustomWinnerHolder.WinnerIds.Add(target.PlayerId); AddFoxesToWinners(); } } //[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))] public static class FixedUpdatePatch { private static Dictionary Positions; private static List ImpostorPositions; private static List NonImpostors; public static void Postfix() { if (!AmongUsClient.Instance.AmHost || !GameStates.IsInTask || ExileController.Instance || Options.CurrentGameMode != CustomGameMode.HideAndSeek || Main.HasJustStarted) return; long now = Utils.TimeStamp; if (LastUpdate == now) return; LastUpdate = now; TimeLeft--; try { List toRemove = null; foreach (var id in PlayerRoles.Keys) { if (!id.GetPlayer()) { toRemove ??= []; toRemove.Add(id); } } if (toRemove != null) { foreach (var id in toRemove) { PlayerRoles.Remove(id); Utils.SendRPC(CustomRPC.HNSSync, 2, id); } } } catch { } try { Positions = new Dictionary(PlayerRoles.Count); foreach (var pc in Main.CachedAllPlayerControls()) Positions[pc.PlayerId] = pc.Pos(); ImpostorPositions = []; NonImpostors = []; foreach ((byte id, (IHideAndSeekRole Interface, CustomRoles Role) role) in PlayerRoles) { switch (role.Interface.Team) { case Team.Impostor: ImpostorPositions.Add(Positions[id]); break; case Team.Crewmate: case Team.Neutral: NonImpostors.Add(id); break; } } Danger = new Dictionary(NonImpostors.Count); bool doRPC = Utils.DoRPC; MessageWriter writer = !doRPC ? null : AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.HNSSync, SendOption.Reliable); if (doRPC) { writer.WritePacked(3); writer.WritePacked(TimeLeft); } // Precomputed squared thresholds ( (3n+2)^2 ) const float d0 = 2f * 2f; // 4 const float d1 = 5f * 5f; // 25 const float d2 = 8f * 8f; // 64 const float d3 = 11f * 11f; // 121 const float d4 = 14f * 14f; // 196 foreach (var nonImpId in NonImpostors) { Vector2 p = Positions[nonImpId]; float minSq = float.MaxValue; for (int i = 0; i < ImpostorPositions.Count; i++) { Vector2 imp = ImpostorPositions[i]; float dx = imp.x - p.x; float dy = imp.y - p.y; float sq = dx * dx + dy * dy; if (sq < minSq) minSq = sq; } int danger = minSq <= d0 ? 0 : minSq <= d1 ? 1 : minSq <= d2 ? 2 : minSq <= d3 ? 3 : minSq <= d4 ? 4 : 5; Danger[nonImpId] = danger; if (doRPC) { writer.Write(nonImpId); writer.WritePacked(danger); } } if (doRPC) AmongUsClient.Instance.FinishRpcImmediately(writer); } catch { } if (DangerMeter.GetBool() || (TimeLeft + 1) % 60 == 0 || TimeLeft <= 60) Utils.NotifyRoles(); } } }