From 353e8113973603a49d1b40b075ee80e4c39db2c9 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 11 Sep 2025 09:28:56 +0200 Subject: [PATCH 01/24] fix phantom? --- Modules/RPC.cs | 5 +++ Patches/ChatCommandPatch.cs | 9 +++-- Patches/OnGameStartedPatch.cs | 8 +++-- Patches/PhantomRolePatch.cs | 64 ++++++++++++++++++++--------------- 4 files changed, 52 insertions(+), 34 deletions(-) diff --git a/Modules/RPC.cs b/Modules/RPC.cs index 18d9acd83..7ed88b9f1 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -380,6 +380,11 @@ internal static class RPCHandlerPatch break; } + case 62 when GameStates.IsInTask && Main.IntroDestroyed && !ExileController.Instance && !AntiBlackout.SkipTasks: + { + PhantomRolePatch.CheckTrigger(__instance); + break; + } } } diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 2a43571cc..901870bcf 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -106,6 +106,7 @@ internal static class ChatCommands private static readonly Dictionary PollAnswers = []; private static readonly List PollVoted = []; private static float PollTimer = 45f; + private static List GMPollGameModes = []; public static readonly Dictionary MutedPlayers = []; @@ -887,7 +888,8 @@ internal static class ChatCommands return; } - string gmNames = string.Join(' ', Enum.GetValues()[..^1].Where(x => Options.GMPollGameModesSettings[x].GetBool()).Select(x => GetString(x.ToString()).Replace(' ', '_'))); + GMPollGameModes = Enum.GetValues()[..^1].Where(x => Options.GMPollGameModesSettings[x].GetBool()).ToList(); + string gmNames = string.Join(' ', GMPollGameModes.Select(x => GetString(x.ToString()).Replace(' ', '_'))); var msg = $"/poll {GetString("GameModePoll.Question").TrimEnd('?')}? {gmNames}"; PollCommand(player, msg, msg.Split(' ')); } @@ -1637,7 +1639,7 @@ internal static class ChatCommands bool gmPoll = msg.Contains(GetString("GameModePoll.Question")); PollTimer = gmPoll ? 60f : 45f; - Color[] gmPollColors = gmPoll ? Main.GameModeColors.Values.ToArray() : []; + Color[] gmPollColors = gmPoll ? Main.GameModeColors.Where(x => GMPollGameModes.Contains(x.Key)).Select(x => x.Value).ToArray() : []; for (var i = 0; i < Math.Max(answers.Length, 2); i++) { @@ -1701,7 +1703,8 @@ internal static class ChatCommands if (winners.Length == 1 && gmPoll && GameStates.IsLobby) { int winnerIndex = winners[0].Key - 65; - Options.GameMode.SetValue(winnerIndex); + CustomGameMode mode = GMPollGameModes[winnerIndex]; + Options.GameMode.SetValue((int)mode - 1); } } diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index 843e72589..630cc29ea 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -1106,7 +1106,10 @@ internal static class StartGameHostPatch Utils.ThrowException(ex); yield break; } - + + + LoadingBarManager loadingBarManager = FastDestroyableSingleton.Instance; + yield return loadingBarManager.WaitAndSmoothlyUpdate(90f, 95f, 1f, GetString("LoadingBarText.1")); foreach (PlayerControl pc in PlayerControl.AllPlayerControls) { @@ -1120,8 +1123,7 @@ internal static class StartGameHostPatch Logger.Info("Successfully set everyone's data as Disconnected", "StartGameHost"); - LoadingBarManager loadingBarManager = FastDestroyableSingleton.Instance; - yield return loadingBarManager.WaitAndSmoothlyUpdate(90f, 100f, 2f, GetString("LoadingBarText.1")); + yield return loadingBarManager.WaitAndSmoothlyUpdate(95f, 100f, 1f, GetString("LoadingBarText.1")); loadingBarManager.ToggleLoadingBar(false); Main.AllPlayerControls.Do(SetRoleSelf); diff --git a/Patches/PhantomRolePatch.cs b/Patches/PhantomRolePatch.cs index a4484fa76..788ac8686 100644 --- a/Patches/PhantomRolePatch.cs +++ b/Patches/PhantomRolePatch.cs @@ -61,35 +61,8 @@ public static class PhantomRolePatch PlayerControl phantom = __instance; Logger.Info($"Player: {phantom.GetNameWithRole()}", "CheckVanish"); - if ((phantom.Is(CustomRoles.Trainee) && MeetingStates.FirstMeeting) || !Rhapsode.CheckAbilityUse(phantom) || Stasis.IsTimeFrozen || TimeMaster.Rewinding || !Main.PlayerStates[__instance.PlayerId].Role.OnVanish(__instance)) - { - if (phantom.AmOwner) - { - FastDestroyableSingleton.Instance.AbilityButton.SetFromSettings(phantom.Data.Role.Ability); - phantom.Data.Role.SetCooldown(); - return false; - } - - var sender = CustomRpcSender.Create($"Cancel vanish for {phantom.GetRealName()}", SendOption.Reliable); - sender.StartMessage(phantom.OwnerId); - - sender.StartRpc(phantom.NetId, RpcCalls.SetRole) - .Write((ushort)RoleTypes.Phantom) - .Write(true) - .EndRpc(); - - sender.StartRpc(phantom.NetId, RpcCalls.ProtectPlayer) - .WriteNetObject(phantom) - .Write(0) - .EndRpc(); - - sender.EndMessage(); - sender.SendMessage(); - - LateTask.New(() => phantom.SetKillCooldown(Math.Max(Main.KillTimers[phantom.PlayerId], 0.001f)), 0.2f); - + if (!CheckTrigger(phantom)) return false; - } foreach (PlayerControl target in Main.AllPlayerControls) { @@ -168,6 +141,41 @@ public static class PhantomRolePatch return true; } + public static bool CheckTrigger(PlayerControl phantom) + { + if ((phantom.Is(CustomRoles.Trainee) && MeetingStates.FirstMeeting) || !Rhapsode.CheckAbilityUse(phantom) || Stasis.IsTimeFrozen || TimeMaster.Rewinding || !Main.PlayerStates[phantom.PlayerId].Role.OnVanish(phantom)) + { + if (phantom.AmOwner) + { + FastDestroyableSingleton.Instance.AbilityButton.SetFromSettings(phantom.Data.Role.Ability); + phantom.Data.Role.SetCooldown(); + return false; + } + + var sender = CustomRpcSender.Create($"Cancel vanish for {phantom.GetRealName()}", SendOption.Reliable); + sender.StartMessage(phantom.OwnerId); + + sender.StartRpc(phantom.NetId, RpcCalls.SetRole) + .Write((ushort)RoleTypes.Phantom) + .Write(true) + .EndRpc(); + + sender.StartRpc(phantom.NetId, RpcCalls.ProtectPlayer) + .WriteNetObject(phantom) + .Write(0) + .EndRpc(); + + sender.EndMessage(); + sender.SendMessage(); + + LateTask.New(() => phantom.SetKillCooldown(Math.Max(Main.KillTimers[phantom.PlayerId], 0.001f)), 0.2f); + + return false; + } + + return true; + } + [HarmonyPatch(nameof(PlayerControl.CheckAppear))] [HarmonyPrefix] private static void CheckAppear_Prefix(PlayerControl __instance, bool shouldAnimate) From d69dd703a91ebb0064d49e444c87d64c521129c2 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 11 Sep 2025 09:43:13 +0200 Subject: [PATCH 02/24] add missing strings --- EHR.csproj | 2 +- Resources/Lang/en_US.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/EHR.csproj b/EHR.csproj index 54738d0e0..04ad23bf5 100644 --- a/EHR.csproj +++ b/EHR.csproj @@ -8,7 +8,7 @@ EHR Gurge44 preview - D:\SteamLibrary\steamapps\common\Among Us + C:\Program Files\Epic Games\AmongUs true True $(MSBuildProjectName) diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 430921d89..b7eb3916e 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", From e9b25eca94762184e3ff895d27fb43feb12866da Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 11 Sep 2025 09:57:52 +0200 Subject: [PATCH 03/24] a --- Resources/Lang/en_US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index b7eb3916e..a3c8ee741 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(Impostors):\\nAnti-Adminer can detect nearby crewmates or neutrals around devices such as Cameras, Admin Table, Vitals, DoorLog, etc. Note that they cannot confirm if the player is using the device, only that someone is close to it.\\n\\nDepending on a setting, you may have an additional ability: You can shapeshift to alert every player using a device to leave it. They have a few seconds to step away from the devices, or else they die.", "SansInfoLong": "(Impostors):\\nThe Arrogance reduces their kill cooldown with each successful kill of theirs.", "BomberInfoLong": "(Impostors):\\nThe Bomber can use the vanish/pet button to self-explode, killing players within a certain range. But as a price, the Bomber will also die. Note: All players will see a kill flash when the Bomber explodes.", - "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. In addition, if the victim is a bait, no self-report will be made. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(Impostors):\\nIn the game, the Trapster kills a player and then puts a trap on their body. If another player reports the dead body and it has the Trapster's trap on it, the player who reports will die instantly without the body getting reported. The player killed by the Trapster's trap will also become a Trapster. This chain reaction continues until no more players are left to become Trapsters. However, the Trapster will die immediately after making a kill. It's worth noting that the Trapster's own dead body cannot trigger their trap.", "CapitalismInfoLong": "(Impostors):\\nCapitalists can add a short task to a player by pressing the kill button. Note: using this ability on players who have no tasks or have completed tasks will not take effect.", "GangsterInfoLong": "(Impostors):\\nThe Gangster can recruit a player to a Madmate by pressing the kill button. If successful, both will see the shield animation as a reminder. The host sets the maximum number of recruits. If recruitment fails, the target is killed normally. When out of recruits, the Gangster can only make normal kills.", From 0ba4b9bf505e55a963aa51bd5fd586eb7b211921 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 11 Sep 2025 10:11:28 +0200 Subject: [PATCH 04/24] b --- Patches/PlayerControlPatch.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index e8acdcbb1..ff45d2e86 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -1348,6 +1348,9 @@ internal static class ReportDeadBodyPatch if (Main.CurrentMap == MapNames.Fungle && (pc.IsMushroomMixupActive() || IsActive(SystemTypes.MushroomMixupSabotage))) pc.FixMixedUpOutfit(); + + if (Main.Invisible.Contains(pc.PlayerId)) + pc.RpcMakeVisible(); PhantomRolePatch.OnReportDeadBody(pc); } @@ -2108,7 +2111,7 @@ internal static class PlayerStartPatch TextMeshPro nameText = __instance.__4__this.cosmetics.nameText; TextMeshPro roleText = Object.Instantiate(nameText, nameText.transform, true); bool largerFontSize = Options.LargerRoleTextSize.GetBool(); - roleText.transform.localPosition = new(0f, largerFontSize ? 0.4f : 0.2f, 0f); + roleText.transform.localPosition = new(0f, largerFontSize ? 0.35f : 0.2f, 0f); if (!largerFontSize) roleText.fontSize -= 0.9f; roleText.text = "RoleText"; roleText.gameObject.name = "RoleText"; From d66b27473a4b98bede658cab97d6f34a18fd7561 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 11 Sep 2025 10:26:47 +0200 Subject: [PATCH 05/24] fix --- Modules/ExtendedPlayerControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 46113568e..3fd46d7d6 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -91,6 +91,8 @@ internal static class ExtendedPlayerControl return true; case CustomGameMode.Standard when Main.AllAlivePlayerControls.Length == 2 && player.GetRoleTypes() != RoleTypes.Engineer: return false; + case CustomGameMode.MoveAndStop: + return MoveAndStop.IsEventActive && MoveAndStop.Event.Type == MoveAndStop.Events.VentAccess; } if (player.Is(CustomRoles.Trainee) && MeetingStates.FirstMeeting) return false; From 530d736dbde319bb22b885ea1d3de76bd8e1a9d0 Mon Sep 17 00:00:00 2001 From: Gurge44 <90392754+Gurge44@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:27:10 +0200 Subject: [PATCH 06/24] New Crowdin updates (#352) * Update source file en_US.json * New translations en_us.json (Russian) * New translations en_us.json (Chinese Simplified) * New translations en_us.json (Polish) * New translations en_us.json (Japanese) * New translations en_us.json (Spanish) * New translations en_us.json (German) * New translations en_us.json (Hungarian) * New translations en_us.json (Korean) * New translations en_us.json (Chinese Traditional) * New translations en_us.json (Portuguese, Brazilian) * New translations en_us.json (Indonesian) * New translations en_us.json (Persian) --- Resources/Lang/de_DE.json | 4 +++- Resources/Lang/es_ES.json | 4 +++- Resources/Lang/fa_IR.json | 4 +++- Resources/Lang/hu_HU.json | 4 +++- Resources/Lang/id_ID.json | 4 +++- Resources/Lang/ja_JP.json | 4 +++- Resources/Lang/ko_KR.json | 4 +++- Resources/Lang/pl_PL.json | 4 +++- Resources/Lang/pt_BR.json | 4 +++- Resources/Lang/ru_RU.json | 4 +++- Resources/Lang/zh_CN.json | 4 +++- Resources/Lang/zh_TW.json | 4 +++- 12 files changed, 36 insertions(+), 12 deletions(-) diff --git a/Resources/Lang/de_DE.json b/Resources/Lang/de_DE.json index af3d3e9e4..0451f54d3 100644 --- a/Resources/Lang/de_DE.json +++ b/Resources/Lang/de_DE.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(Impostors):\\nAnti-Adminer can detect nearby crewmates or neutrals around devices such as Cameras, Admin Table, Vitals, DoorLog, etc. Note that they cannot confirm if the player is using the device, only that someone is close to it.\\n\\nDepending on a setting, you may have an additional ability: You can shapeshift to alert every player using a device to leave it. They have a few seconds to step away from the devices, or else they die.", "SansInfoLong": "(Impostors):\\nThe Arrogance reduces their kill cooldown with each successful kill of theirs.", "BomberInfoLong": "(Impostors):\\nThe Bomber can use the vanish/pet button to self-explode, killing players within a certain range. But as a price, the Bomber will also die. Note: All players will see a kill flash when the Bomber explodes.", - "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. In addition, if the victim is a bait, no self-report will be made. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(Impostors):\\nIn the game, the Trapster kills a player and then puts a trap on their body. If another player reports the dead body and it has the Trapster's trap on it, the player who reports will die instantly without the body getting reported. The player killed by the Trapster's trap will also become a Trapster. This chain reaction continues until no more players are left to become Trapsters. However, the Trapster will die immediately after making a kill. It's worth noting that the Trapster's own dead body cannot trigger their trap.", "CapitalismInfoLong": "(Impostors):\\nCapitalists can add a short task to a player by pressing the kill button. Note: using this ability on players who have no tasks or have completed tasks will not take effect.", "GangsterInfoLong": "(Impostors):\\nThe Gangster can recruit a player to a Madmate by pressing the kill button. If successful, both will see the shield animation as a reminder. The host sets the maximum number of recruits. If recruitment fails, the target is killed normally. When out of recruits, the Gangster can only make normal kills.", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", diff --git a/Resources/Lang/es_ES.json b/Resources/Lang/es_ES.json index bab74fef8..2ed1cc283 100644 --- a/Resources/Lang/es_ES.json +++ b/Resources/Lang/es_ES.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(Impostores):\\nEl Anti Administrador puede detectar tripulantes o neutrales cercanos alrededor de dispositivos como las Cámaras, Mesa de Admin, Vitales, Registro de puertas, etc. Ten en cuenta que no puede confirmar si el jugador está usando el dispositivo, solo que alguien está cerca de este.\\n\\nDependiendo de una configuración, podrías tener una habilidad adicional: Puedes cambiar de forma para alertar a todos los jugadores usando un dispositivo para que lo dejen. Tienen unos pocos segundos para alejarse de los dispositivos, o si no morirán.", "SansInfoLong": "(Impostores):\\nLa Arrogancia reduce su tiempo de reutilización de asesinato con cada uno de sus asesinatos exitosos.", "BomberInfoLong": "(Impostors):\\nThe Bomber can use the vanish/pet button to self-explode, killing players within a certain range. But as a price, the Bomber will also die. Note: All players will see a kill flash when the Bomber explodes.", - "ScavengerInfoLong": "(Impostores):\\nAsesinatos del Basurero no dejan cuerpos muertos detrás. Adicionalmente, si la víctima es un cebo, no se hará autorreporte. Para matar a alguien, debes usar tu botón de matar en él, y quedarte cerca de esa persona por algunos segundos, similar al rociamiento del Incendiario.", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(Impostores):\\nEn el juego, el Trampero mata a un jugador y luego pones una trampa en su juego. Si otro jugador reporta el cuerpo muerto y este tiene la trampa del Trampero en él, el jugador que reporta morirá instantáneamente sin que el cuerpo sea reportado. El jugador asesinado por la trampa del Trampero también se convertirá en un Trampero. La reacción en cadena continúa hasta que no hayan jugadores restantes que se vuelvan Tramperos. Sin embargo, el Trampero morirá inmediatamente después de hacer un asesinato. Vale la pena mencionar que el propio cuerpo del Trampero no puede activar su trampa.", "CapitalismInfoLong": "(Impostores):\\nEl Capitalista puede añadir una tarea corta a un jugador presionando su botón de matar. Nota: usar esta habilidad en jugadores que no tienen tareas o han completado sus tareas no tendrá efecto.", "GangsterInfoLong": "(Impostores):\\nEl Pandillero puede reclutar a un jugador como Madmate presionando su botón de matar. Si es exitoso, ambos verán la animación de escudo como recordatorio. El host establece el número máximo de reclutamientos. Si el reclutamiento falla, el objetivo es asesinado normalmente. Cuando se le acaban los reclutamientos, el Pandillero solo puede hacer asesinatos normales.", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", diff --git a/Resources/Lang/fa_IR.json b/Resources/Lang/fa_IR.json index ca7312ea1..58ab1e239 100644 --- a/Resources/Lang/fa_IR.json +++ b/Resources/Lang/fa_IR.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(ایمپاسترها):\\nآنتی ادمینر (Anti-Adminer) می‌تواند کرومیت‌ها (Crewmates) یا خنثی‌های (Neutrals) نزدیک به دستگاه‌هایی مانند دوربین‌ها (Cameras)، میز ادمین (Admin Table)، ویتالز (Vitals) و گزارش درها (DoorLog) را تشخیص دهد. توجه داشته باشید که او نمی‌تواند تأیید کند که آیا بازیکن در حال استفاده از دستگاه است یا نه، فقط تشخیص می‌دهد که کسی نزدیک آن است.\\n\\nبسته به تنظیمات، شما ممکن است یک قابلیت اضافی داشته باشید: می‌توانید تغییرشکل دهید (shapeshift) تا به تمام بازیکنانی که از یک دستگاه استفاده می‌کنند هشدار دهید که آن را ترک کنند. آن‌ها چند ثانیه فرصت دارند تا از دستگاه‌ها فاصله بگیرند، وگرنه می‌میرند.", "SansInfoLong": "(ایمپاسترها):\\nقابلیت تکبر (The Arrogance) با هر کیل موفق، کول‌داون کشتن‌شان را کاهش می‌دهد.", "BomberInfoLong": "(ایمپاسترها):\\nبمب‌گذار (Bomber) می‌تواند از دکمه ناپدید شدن/حیوان خانگی (vanish/pet) برای انفجار خود استفاده کند و بازیکنان حاضر در یک محدوده مشخص را بکشد. اما به عنوان هزینه، خود بمب‌گذار (Bomber) نیز خواهد مرد. توجه: وقتی بمب‌گذار (Bomber) منفجر می‌شود، تمام بازیکنان یک فلاشِ کشته شدن (kill flash) خواهند دید.", - "ScavengerInfoLong": "(ایمپاسترها):\\nلاشخور (Scavenger) جسدهای مرده را پشت سر خود باقی نمی‌گذارد. علاوه بر این، اگر قربانی یک طعمه (bait) باشد، گزارش خودکار (self-report) انجام نمی‌شود. برای کشتن، باید روی دکمه کیل خود کلیک کنید، سپس برای چند ثانیه نزدیک آن شخص بمانید، شبیه به مکانیک بنزین ریختن آرسونیست (Arsonist).", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(ایمپاسترها):\\nدر طول بازی، ترپستر (Trapster) یک بازیکن را می‌کشد و سپس روی جسد او یک تله می‌گذارد. اگر بازیکن دیگری جسد را گزارش کند و تله ترپستر (Trapster) روی آن باشد، گزارش‌دهنده فوراً می‌میرد بدون اینکه جسد گزارش شود. بازیکنی که توسط تله ترپستر (Trapster) کشته می‌شود هم تبدیل به یک ترپستر (Trapster) می‌شود. این زنجیره تا زمانی که دیگر بازیکنی برای تبدیل شدن باقی نمانده باشد ادامه می‌یابد. با این حال، خود ترپستر (Trapster) بلافاصله پس از گذاشتن تله خواهد مرد. لازم به ذکر است که جسد خود ترپستر (Trapster) نمی‌تواند تله‌اش را فعال کند.", "CapitalismInfoLong": "(ایمپاسترها):\\nسرمایه‌دارها (Capitalists) می‌توانند با فشار دادن دکمه کیل، یک تسک کوتاه به یک بازیکن اضافه کنند. توجه: استفاده از این قابلیت روی بازیکنی که هیچ تسکی ندارد یا تسک‌هایش را تمام کرده، تأثیری نخواهد داشت.", "GangsterInfoLong": "(ایمپاسترها):\\nگنگستر (Gangster) می‌تواند با فشار دادن دکمه کیل، یک بازیکن را به مدمیت (Madmate) تبدیل کند. اگر موفقیت‌آمیز باشد، هر دو به عنوان یادآوری، انیمیشن سپر را خواهند دید. هاست (Host) حداکثر تعداد استخدام‌ها را تعیین می‌کند. اگر استخدام ناموفق باشد، هدف کشته می‌شود. وقتی تعداد استخدام‌هایش تمام شود، گنگستر (Gangster) فقط می‌تواند کیل‌های عادی انجام دهد.", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", diff --git a/Resources/Lang/hu_HU.json b/Resources/Lang/hu_HU.json index 4b07cdd1d..ae695d983 100644 --- a/Resources/Lang/hu_HU.json +++ b/Resources/Lang/hu_HU.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(Imposztorok):\\nÉrzékeli a legénységi és semleges játékosokat, ha adminisztrációs eszközök közelében vannak (kamerák, admin asztal, vitals stb.). Nem tudja megmondani, hogy valóban használják-e az eszközt, csak hogy közel vannak.\\n\\nBeállítástól függően alakváltással figyelmeztetheti az eszközt használókat, hogy hagyják el azt, különben meghalnak.", "SansInfoLong": "(Imposztorok):\\nMinden sikeres gyilkosság után csökkenti a gyilkossági töltési idejét.", "BomberInfoLong": "(Impostors):\\nThe Bomber can use the vanish/pet button to self-explode, killing players within a certain range. But as a price, the Bomber will also die. Note: All players will see a kill flash when the Bomber explodes.", - "ScavengerInfoLong": "(Imposztorok):\\nGyilkosságai nem hagynak holttesteket. Ha a célpont Csali volt, önjelentés sem történik. A gyilkossághoz néhány másodpercig a célpont közelében kell maradnod.", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(Imposztorok):\\nEgy játékost megöl, majd csapdát helyez a holttestére. Ha valaki bejelenti a holttestet, miközben az csapdázott, azonnal meghal, és a test nem kerül jelentésre. A csapda által megölt játékosok szintén csapdává válnak. Ez a láncreakció addig folytatódik, amíg nincs több játékos, aki csapda lehetne. Fontos: a szerep saját holtteste nem aktiválja a csapdáját.", "CapitalismInfoLong": "(Imposztorok):\\nRövid feladatokat adhat egy játékosnak a gyilkosság gombbal. Ha a célpontnak nincs több feladata, vagy már befejezte az összeset, az képesség hatástalan lesz.", "GangsterInfoLong": "(Imposztorok):\\nEgy játékost őrült csapattárssá változtathat a gyilkosság gomb megnyomásával. Ha a toborzás sikeres, mindketten látják a pajzs animációt emlékeztetőként. A házigazda beállíthatja a maximális toborzások számát. Sikertelen toborzás esetén a célpontot normál módon megöli. Ha elfogy a toborzási lehetősége, csak normál módon gyilkolhat.", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", diff --git a/Resources/Lang/id_ID.json b/Resources/Lang/id_ID.json index 463b2abaa..c9ce8d31c 100644 --- a/Resources/Lang/id_ID.json +++ b/Resources/Lang/id_ID.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(Impostor):\\nSelaku Anti-Adminer, kamu dapat mendeteksi Crewmate atau Netral yang berada di dekat perangkat seperti CCTV, Meja Admin, Monitor Vital, atau Riwayat Akses Pintu. Namun, kamu tidak bisa mengetahui apakah pemain tersebut sedang menggunakan perangkat itu atau tidak; kamu hanya tahu bahwa ada orang di dekat situ.\\n\\nTergantung konfigurasi, kamu mungkin bisa menggunakan Shapeshift untuk mengaktifkan ultimatum yang mengharuskan semua pemain di dekat perangkat untuk segera meninggalkan tempat dalam hitungan detik, dengan konsekuensi kematian jika tidak diindahkan.", "SansInfoLong": "(Impostors):\\nThe Arrogance reduces their kill cooldown with each successful kill of theirs.", "BomberInfoLong": "(Impostors):\\nThe Bomber can use the vanish/pet button to self-explode, killing players within a certain range. But as a price, the Bomber will also die. Note: All players will see a kill flash when the Bomber explodes.", - "ScavengerInfoLong": "(Impostor):\\nSelaku Scavenger, kamu harus menunggu beberapa detik di dekat target setelah menekan tombol Kill agar pembunuhan berhasil. Target akan mati tanpa meninggalkan jejak mayat.", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(Impostor):\\nSelaku Trapster, setelah kamu membunuh satu pemain, mayatnya akan otomatis dipasangi perangkap. Jika ada pemain lain yang report mayat tersebut, ia akan langsung mati, dan mayat pertama tadi tidak akan kena report. Pemain yang mati karena perangkapmu ini juga akan dipasangi perangkap serupa, menciptakan reaksi berantai yang terus berlanjut sampai tidak ada lagi pemain yang terkena (misalnya, terhentikan oleh panggilan rapat).", "CapitalismInfoLong": "(Impostor):\\nSelaku Capitalist, kamu dapat menambahkan misi ringan kepada pemain lain dengan menggunakan tombol Kill. Catatan: kemampuan ini tidak berlaku pada pemain yang tidak memiliki misi atau yang telah merampungkan misinya.", "GangsterInfoLong": "(Impostors):\\nThe Gangster can recruit a player to a Madmate by pressing the kill button. If successful, both will see the shield animation as a reminder. The host sets the maximum number of recruits. If recruitment fails, the target is killed normally. When out of recruits, the Gangster can only make normal kills.", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", diff --git a/Resources/Lang/ja_JP.json b/Resources/Lang/ja_JP.json index 7e08c6b22..7ac0ac6e6 100644 --- a/Resources/Lang/ja_JP.json +++ b/Resources/Lang/ja_JP.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(<#ff1919>インポスター):\\nアンチアドミナーは カメラ、アドミン テーブル、バイタル、ドアログ、等のような デバイスの周りに近くの <#8cffff>クルーメイト あるいは <#ffab1b>ニュートラルを検出できます。\\nもし プレイヤーがデバイスを使用しているなら 彼らは その確認できないので注意し、それは 誰かが近くにいるということだけです。\\n\\n設定に依存して、あなたは 追加能力があるかもしれません: あなたはすべてのデバイスを使用しているプレイヤーに そこから離れるよう警告する シェイプシフトができます。\\n彼らは デバイスから離れられる ステップが数秒あり、さもなければ 彼らは 死にます。", "SansInfoLong": "(<#ff1919>インポスター):\\nアロガンスは 彼らの殺害に成功するたびに 彼らのキルのクールダウンが減少します。", "BomberInfoLong": "(<#ff1919>インポスター):\\nボマーは 消える/ペットを使用して 自-爆し、一定の範囲内のプレイヤーを殺害しますが、ボマーは それで死亡します。\\n 注意: すべてのプレイヤーは ボマーが爆発したとき キルフラッシュが見れます。", - "ScavengerInfoLong": "(<#ff1919>インポスター):\\nスカベンジャーの殺害は 死んだ身体の陰が残りません。\\nさらに、もし 被害者が <#00f7ff>ベイトであっても、セルフ-レポートが行われません。\\n誰かを殺すには、<#ff6633>アーソニストの浸漬と同じく、あなたは その人にあなたのキルボタンをクリックし、それから 数秒間 その人の近くに留まらなければなりません。", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(<#ff1919>インポスター):\\nゲームで、トラップスターは 1人のプレイヤーを殺害して それから その人の死んだ身体に トラップを置きます。\\nもし 別のプレイヤーが 死んだ身体を報告して それにトラップスターの罠があるときに、プレイヤーの誰かが報告すると 身体は報告されることがなく 即座に死にます。\\nトラップスターの罠によって殺されたプレイヤーは さらに トラップスターになります。\\nこの連鎖反応は トラップスターになる 残りのより多くのプレイヤーがいなくなるまで続きます。\\nところが、トラップスターは 殺害をした後 すぐに死にます。\\nトラップスター自身の死んだ身体は 彼らの罠が発動できないことに注意してください。", "CapitalismInfoLong": "(<#ff1919>インポスター):\\n資本主義者は キルボタンを押すことにより 1人のプレイヤーに 1つのショートタスクを追加できます。\\n注意: タスクを持たないプレイヤーの誰か あるいは タスクをコンプリートしたプレイヤーに この能力を使用すると 発揮されません。", "GangsterInfoLong": "(<#ff1919>インポスター):\\nギャングスターは キルボタンを押すことによって マッドメイトにするための 1人のプレイヤーを募集できます。\\nもし 成功すると、両方に 通知として シールドアニメーションが見れます。\\nホストが 最大の募集の数をセットします。\\nもし 採用が失敗すると、ターゲットは 普通に殺害されます。\\n募集の数が切れたとき、ギャングスターは 普通の殺害のみができます。", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", diff --git a/Resources/Lang/ko_KR.json b/Resources/Lang/ko_KR.json index 23a4fb194..f23196f86 100644 --- a/Resources/Lang/ko_KR.json +++ b/Resources/Lang/ko_KR.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(Impostors):\\nAnti-Adminer can detect nearby crewmates or neutrals around devices such as Cameras, Admin Table, Vitals, DoorLog, etc. Note that they cannot confirm if the player is using the device, only that someone is close to it.\\n\\nDepending on a setting, you may have an additional ability: You can shapeshift to alert every player using a device to leave it. They have a few seconds to step away from the devices, or else they die.", "SansInfoLong": "(Impostors):\\nThe Arrogance reduces their kill cooldown with each successful kill of theirs.", "BomberInfoLong": "(Impostors):\\nThe Bomber can use the vanish/pet button to self-explode, killing players within a certain range. But as a price, the Bomber will also die. Note: All players will see a kill flash when the Bomber explodes.", - "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. In addition, if the victim is a bait, no self-report will be made. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(Impostors):\\nIn the game, the Trapster kills a player and then puts a trap on their body. If another player reports the dead body and it has the Trapster's trap on it, the player who reports will die instantly without the body getting reported. The player killed by the Trapster's trap will also become a Trapster. This chain reaction continues until no more players are left to become Trapsters. However, the Trapster will die immediately after making a kill. It's worth noting that the Trapster's own dead body cannot trigger their trap.", "CapitalismInfoLong": "(Impostors):\\nCapitalists can add a short task to a player by pressing the kill button. Note: using this ability on players who have no tasks or have completed tasks will not take effect.", "GangsterInfoLong": "(임포스터):\\n건달은 처치 단추를 눌러 다른 참가자를 광민으로 만들 수 있습니다. 성공할 경우, 둘 다 이 사실을 방어막 애니메이션을 통해 알 수 있습니다. 주최자는 최대한의 모집 가능 수를 정할 수 있습니다. 만약 모집이 실패할 경우, 대상은 그냥 죽습니다. 더 이상 모집이 불가능한 상태의 건달은 적을 그냥 죽이는 것만 가능합니다.", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", diff --git a/Resources/Lang/pl_PL.json b/Resources/Lang/pl_PL.json index 4cdb4f6e5..f8b95bf9c 100644 --- a/Resources/Lang/pl_PL.json +++ b/Resources/Lang/pl_PL.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(Impostors):\\nAnti-Adminer potrafi wykrywac pobliskich crewmate'ow lub neutralnych w okolicach urzadzen takich jak Kamery, Admin Panel, Vitals, Logi itp. Nie moze potwierdzic, czy gracz uzywa urzadzenia – tylko ze ktos jest blisko.\n\nW zaleznosci od ustawien mozesz miec dodatkowa zdolnosc: mozesz uzyc Shapeshift, by ostrzec wszystkich uzywajacych urzadzen. Maja wtedy kilka sekund, by odejsc – inaczej zgina.", "SansInfoLong": "(Impostors):\\nArrogance skraca czas odnowienia zabójstwa po kazdym swoim udanym zabójstwie.", "BomberInfoLong": "(Impostors):\\nBomber moze uzyc przycisku shiftu/glaskania, aby wybuchnac samemu, zabijajac graczy w okreslonym zasiegu. W zamian Bomber równiez umrze. Uwaga: wszyscy gracze zobacza blysk zabójstwa, gdy Bomber wybuchnie.", - "ScavengerInfoLong": "(Impostors):\\nZabojstwa Scavengera nie zostawiaja cial. Jesli ofiara to Bait, nie nastapi selfreport. Aby kogos zabic, musisz kliknac przycisk zabojstwa na graczu i pozostac blisko niego przez kilka sekund – podobnie jak przy polewaniu benzyna y Arsonisty. Gracz znika", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(Impostors):\\nW grze Trapster zabija gracza, a nastepnie zaklada pulapke na jego cialo. Jesli inny gracz sprobuje zglosic to cialo, a znajduje sie na nim pulapka Trapstera, gracz zglaszajacy ginie natychmiast, a cialo nie zostaje zgloszone. Gracz zabity przez pulapke sam staje sie Trapsterem. Reakcja lancuchowa trwa, dopoki nie zabraknie graczy do przemiany. Sam Trapster ginie od razu po dokonaniu zabojstwa. Wazne: wlasne cialo Trapstera nie moze aktywowac pulapki.", "CapitalismInfoLong": "(Impostors):\\nCapitalist moze dodac krótki task graczowi, naciskajac przycisk zabójstwa. Uwaga: uzycie tej umiejetnosci na graczach, którzy nie maja zadan lub juz je ukonczyli, nie przyniesie efektu.", "GangsterInfoLong": "(Impostors):\\nGangster moze zrekrutowac gracza na Madmate'a, naciskajac przycisk zabójstwa. Jesli rekrutacja sie powiedzie, oboje zobacza animacje tarczy jako przypomnienie. Host ustala maksymalna liczbe rekrutów. Jesli rekrutacja sie nie powiedzie, cel zostaje zabity w normalny sposób. Po wykorzystaniu limitu rekrutów, Gangster moze tylko zabijac normalnie.", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", diff --git a/Resources/Lang/pt_BR.json b/Resources/Lang/pt_BR.json index 96e1b0f0a..390108b2a 100644 --- a/Resources/Lang/pt_BR.json +++ b/Resources/Lang/pt_BR.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(Impostores):\\nO Anti Administrador pode detectar tripulantes ou neutros próximos a dispositivos como Câmeras, Mesa de Admin, Vitais, Registro de Portas, etc. Observe que eles não podem confirmar se o jogador está usando o dispositivo, apenas que alguém está perto dele.\\n\\nDependendo de uma configuração, você pode ter uma habilidade adicional: Você pode se transformar para alertar todos os jogadores que estão usando um dispositivo para deixá-lo. Eles têm alguns segundos para se afastar dos dispositivos, ou então eles morrem.", "SansInfoLong": "(Impostores):\\nO Arrogante reduz seu tempo de recarga para matar com cada abate bem-sucedido dele.", "BomberInfoLong": "(Impostores):\\nO Bombardeiro pode usar o botão de Desaparecer/pet para se auto-explodir, matando os jogadores com uma certa distância. Mas tem um preço, o Bombardeiro vai também morrer. Nota: Todos os jogadores verão um flash de morte quando o Bombardeiro explodir.", - "ScavengerInfoLong": "(Impostores):\\nOs abates do Necrófago não deixam corpos. Além disso, se a vítima for Armador, nenhum auto-reporte será feito. Para matar alguém, você deve clicar no seu botão de \"matar\" e, em seguida, ficar perto daquela pessoa por alguns segundos, semelhante a um Pirômano.", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(Impostores):\\nNo jogo, o Caçador mata um jogador e então coloca uma armadilha em seu corpo. Se outro jogador reportar o corpo morto e ele tiver com a armadilha do Caçador, o jogador que fizer o reporte será morto instantaneamente sem que o corpo seja reportado. O jogador que for morto pela armadilha do Caçador também se tornará uma armadilha. Essa reação em cadeia continua até que não haja mais jogadores para se tornarem uma armadilha. No entanto, o Caçador morrerá imediatamente após fazer um abate. Vale notar que o próprio corpo morto do Caçador não pode acionar sua armadilha.", "CapitalismInfoLong": "(Impostores):\\nO Capitalista pode adicionar uma tarefa curta a um jogador pressionando o botão de matar. Nota: Usar essa habilidade em um jogador que não tem tarefas ou já terminou todas não terá efeito.", "GangsterInfoLong": "(Impostores):\\nO Gangster pode recrutar um jogador para se tornar um Cúmplice pressionando o botão de matar. Se a recrutagem for bem-sucedida, ambos verão a animação de escudo como um lembrete. O número máximo de recrutas é definido pelo anfitrião. Se a recrutagem falhar, o alvo será morto normalmente. Quando não houver mais usos disponíveis, o Gangster só poderá matar normalmente.", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", diff --git a/Resources/Lang/ru_RU.json b/Resources/Lang/ru_RU.json index 4a826b816..66de631a1 100644 --- a/Resources/Lang/ru_RU.json +++ b/Resources/Lang/ru_RU.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(Предатель):\\nКак Анти-Админер, вы можете узнавать, используется ли какое-либо устройство (админка/камеры/пульс/дурлог и т.д.). Однако, вы не можете знать наверняка, используется ли устройство или игрок просто стоит рядом с ним. \\nЕсли вы исчезните/морфнитесь/погладите питомца, все игроки будут предупреждены о том, что им немедленно нужно отойти от устройства - иначе они умрут. На это у них есть пара секунд.", "SansInfoLong": "(Предатель):\\nКак Высокомер, вы уменьшаете свою перезарядку убийства навсегда с каждым убийством.", "BomberInfoLong": "(Предатель):\\nКак Бомбер, вы можете исчезнуть/погладить питомца, чтобы взорваться. Взрыв убьёт всех игроков в некотором радиусе. Однако, вы тоже умрёте.", - "ScavengerInfoLong": "(Предатель):\\nКак Падальщик, убитые вами игроки телепортируются за карту. Если вы убьёте Байта, саморепорт не произойдёт. В зависимости от настроек, чтобы убить, нужно не отходить от жертвы несколько секунд.", + "ScavengerInfoLong": "(Предатель):\\nКак Падальщик, убитые вами игроки телепортируются за карту. В зависимости от настроек, чтобы убить, нужно не отходить от жертвы несколько секунд.", "BoobyTrapInfoLong": "(Предатель):\\nКак Ловушечник, когда вы убиваете, на труп накладывается бомба. Любой, кто попытается зарепортить этот труп, умрёт. Труп игрока, который умрёт от репорта заминированного трупа, также станет заминированным. Таким образом, образуется цепочка трупов, которые нельзя репортить. Однако, после убийства, вы тоже умрёте (ваше тело не будет заминировано).", "CapitalismInfoLong": "(Предатель):\\nКак Капиталист, вы можете добавить любому игроку 1 дополнительный короткий таск с помощью кнопки убийства (убивать вы всё ещё можете). Если способность была использована на игроке без тасков, ничего не произойдёт.", "GangsterInfoLong": "(Предатель):\\nКак Гангстер, вы можете рекрутировать игроков, превращая их в Безумцев с помощью кнопки убийства. Рекрутированы будут первые игроки на которых вы нажмёте кнопкой убийства. Не все роли можно рекрутировать. Если вы попытаетесь рекрутировать роль, которая не может быть превращена в безумца, произойдёт обычное убийство. Когда закончится количество возможных рекрутов, вы будете убивать обычным способом.", @@ -9220,9 +9220,11 @@ "Unbound": "Уязвимый", "UnboundInfo": "Вы всегда угадываетесь правильно", "UnboundInfoLong": "(Атрибут):\\nКак Уязвимый, даже если, угадывая вас, угадыватель ошибётся, вы всё равно будете угаданы и умрёте.", + "Prefix.Unbound": "Уязвимый", "Listener": "Подслушивающий", "ListenerInfo": "Иногда вы слышите чужой шёпот", "ListenerInfoLong": "(Атрибут):\\nКак Подслушивающий, с некоторым шансом вы услышите чужой шёпот, когда кто-то использует /whisper. В зависимости от настроек, вы либо увидите содержание сообщения, либо только кто кому прошептал.", + "Prefix.Listener": "Подслушивающий", "Listener.WhisperHearChance": "Подслушивающий слышит шёпот с шансом", "Listener.FullMessageHearChance": "Шанс на то, что будет полностью услышено содержание шёпота", "Listener.FullMessage": "{0}\\n{1} шепчет {2}:\\n{3}", diff --git a/Resources/Lang/zh_CN.json b/Resources/Lang/zh_CN.json index 98a4e4116..b6ddb031b 100644 --- a/Resources/Lang/zh_CN.json +++ b/Resources/Lang/zh_CN.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(内鬼职业):\\n监管者能察觉周遭设备使用情况,例如:管理地图,三岔路口日志,监控,生命仪器.注意:你无法确认玩家是否在用这些设备,只能肯定有人靠近这些设备.你可以用变形警告玩家远离这些设备,如果玩家不远离就死了,取决于房主设置.", "SansInfoLong": "(内鬼职业):\\n狂妄杀手每次击杀都能减少cd.", "BomberInfoLong": "(内鬼职业):\\n自爆兵可以使用隐身/抚摸宠物来自爆并造成范围伤害,但代价是自爆兵也会死亡。注意:自爆兵爆炸时所有玩家都会看到击杀闪光。", - "ScavengerInfoLong": "(内鬼职业):\\n清道夫击杀不会留下尸体.此外,如果受害者是诱饵,不会自我报告.要击杀某人,你必须使用击杀按钮后在那个人附近停留几秒钟,类似于纵火犯的浇油行为.", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(内鬼职业):\\n诡雷杀死玩家时会在鸡腿上放置陷阱.如果其他玩家报告了这个鸡腿就会死亡,这个玩家的鸡腿不能被报告并同样具有陷阱.这种连锁反应会持续到没有足够的玩家.但是诡雷会在击杀之后立刻死亡.如果报告了他的鸡腿则不会触发陷阱.", "CapitalismInfoLong": "(内鬼职业):\\n资本家可以通过击杀给人安排一个额外任务.注意:技能不会在没有任务/已完成任务的玩家上生效.", "GangsterInfoLong": "(内鬼职业):\\n歹徒用击杀即可招募一个人成为叛徒.如果有破盾动画就是成功了.能招募的最大叛徒数目取决于房主.如果招募失败,目标会击杀死亡.当招募次数为0,歹徒只能进行正常击杀.", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", diff --git a/Resources/Lang/zh_TW.json b/Resources/Lang/zh_TW.json index 6e04b359c..1821ab541 100644 --- a/Resources/Lang/zh_TW.json +++ b/Resources/Lang/zh_TW.json @@ -710,7 +710,7 @@ "AntiAdminerInfoLong": "(偽裝者陣營):\\n監管者在有非偽裝者陣營玩家靠近任何一個設備時(例如管理室地圖、心電圖、通行紀錄等) 會收到提示,但是請注意: 監管者只知道有人在靠近此設備,並不知道是否在真正使用該設備。", "SansInfoLong": "(偽裝者陣營):\\n狂妄殺手每次殺人都會減少他的殺人冷卻。", "BomberInfoLong": "(Impostors):\\nThe Bomber can use the vanish/pet button to self-explode, killing players within a certain range. But as a price, the Bomber will also die. Note: All players will see a kill flash when the Bomber explodes.", - "ScavengerInfoLong": "(偽裝者陣營):\\n清道夫在殺人之後不會留下屍體。此外如果受害者為誘餌,則清道夫不會報告誘餌的屍體。但是要使用擊殺能力時需要在使用擊殺後在該玩家附近停留幾秒(就像縱火犯的塗油一樣)", + "ScavengerInfoLong": "(Impostors):\\nScavenger kills do not leave dead bodies behind. To kill someone, you must click your kill button on them, then stay near that person for a few seconds, similar to an Arsonist douse.", "BoobyTrapInfoLong": "(偽裝者陣營):\\n陷阱師在殺人之後會在屍體上裝上一個炸彈,報告此屍體的玩家將立刻被炸死。而被炸彈炸死的屍體上也會有屍體。這種連鎖反應會持續到沒有可以擊殺的玩家為止。但是作為代價陷阱師會在擊殺後立刻自殺,且自己的屍體上不會有炸彈。", "CapitalismInfoLong": "(偽裝者陣營):\\n資本主義者可以通過按下殺人按鈕來給某位玩家增加一個短任務。\\n請注意: 對已完成任務或沒有任務的玩家使用技能將不會發動技能,此外資本主義者不能正常殺人。", "GangsterInfoLong": "(偽裝者陣營):\\n教父可以通過嘗試按殺人鍵來招募一位小弟,若招募成功雙方都會看到身上出現盾牌破碎的效果作為提示。剩餘的招募次數會在教父的名字旁顯示(招募數量依據房主設定),若教父嘗試招募一些不能招募的玩家,像是中立陣營或一些特殊船員職業玩家時(例如擺爛人),則會改為正常殺人。當教父沒有剩餘招募次數時則為正常殺人。", @@ -9220,9 +9220,11 @@ "Unbound": "Unbound", "UnboundInfo": "You're Always Correctly Guessed", "UnboundInfoLong": "(Add-ons):\\nAs the Unbound, when someone tries to guess you, even if they guessed you as an incorrect role or add-on, it will be successful anyway.", + "Prefix.Unbound": "Unbound", "Listener": "Listener", "ListenerInfo": "Listen to Whispers", "ListenerInfoLong": "(Add-ons):\\nAs the Listener, you have a chance to hear others' whispers. You may hear the full message, or only get to know who whispered to who.", + "Prefix.Listener": "Listener", "Listener.WhisperHearChance": "Whisper Hear Chance", "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", From 4bd8722793c7f180a482e5c1917e5e821d8d7db5 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 11 Sep 2025 10:27:37 +0200 Subject: [PATCH 07/24] c --- Modules/ExtendedPlayerControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 3fd46d7d6..65fdcd1cd 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -197,7 +197,7 @@ internal static class ExtendedPlayerControl } /// - /// *Sub-roles cannot be obtained. + /// *Add-ons cannot be obtained. /// public static CustomRoles GetCustomRole(this PlayerControl player) { From a23a6e7c9675783dcdc894f52a3eb1d51f9aeff9 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 11 Sep 2025 10:32:06 +0200 Subject: [PATCH 08/24] ver --- Main.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Main.cs b/Main.cs index cb19d8ac1..7503aa4f6 100644 --- a/Main.cs +++ b/Main.cs @@ -42,8 +42,8 @@ public class Main : BasePlugin private const string DebugKeyHash = "c0fd562955ba56af3ae20d7ec9e64c664f0facecef4b3e366e109306adeae29d"; private const string DebugKeySalt = "59687b"; private const string PluginGuid = "com.gurge44.endlesshostroles"; - public const string PluginVersion = "6.3.0"; - public const string PluginDisplayVersion = "6.3.0"; + public const string PluginVersion = "6.3.1"; + public const string PluginDisplayVersion = "6.3.1"; public const bool TestBuild = false; public const string NeutralColor = "#ffab1b"; From e7dce996180cc3ad06bb426a554c73b922e045c0 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 11 Sep 2025 11:05:49 +0200 Subject: [PATCH 09/24] another set pet call --- Modules/Utils.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 7f736cf7c..0d69d6951 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -3340,6 +3340,7 @@ public static class Utils { string petId = PetsHelper.GetPetId(); PetsHelper.SetPet(pc, petId); + pc.RpcSetPet(petId); }, 3f, "No Pet Reassign"); } From c9e52ac4b00aecd48cf2e055a392a81c7d30bc52 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 11 Sep 2025 12:48:51 +0200 Subject: [PATCH 10/24] literally 1 line change --- Patches/OnGameStartedPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index 630cc29ea..3114787d0 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -369,7 +369,7 @@ internal static class ChangeRoleSettings pc.cosmetics.nameText.text = pc.name; RandomSpawn.CustomNetworkTransformHandleRpcPatch.HasSpawned.Clear(); NetworkedPlayerInfo.PlayerOutfit outfit = pc.Data.DefaultOutfit; - Camouflage.PlayerSkins[pc.PlayerId] = new NetworkedPlayerInfo.PlayerOutfit().Set(outfit.PlayerName, outfit.ColorId, outfit.HatId, outfit.SkinId, outfit.VisorId, outfit.PetId, outfit.NamePlateId); + Camouflage.PlayerSkins[pc.PlayerId] = new NetworkedPlayerInfo.PlayerOutfit().Set(Main.AllPlayerNames.GetValueOrDefault(pc.PlayerId, outfit.PlayerName), outfit.ColorId, outfit.HatId, outfit.SkinId, outfit.VisorId, outfit.PetId, outfit.NamePlateId); Main.ClientIdList.Add(pc.OwnerId); try { Main.PlayerColors[pc.PlayerId] = Palette.PlayerColors[colorId]; } From 5136fd7f87c307a517cd0ccc157029fd127457ef Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Fri, 12 Sep 2025 10:54:51 +0200 Subject: [PATCH 11/24] Add Clerk (Tax Collector) --- CustomRoles.cs | 1 + GameMode/HotPotato.cs | 2 +- Main.cs | 2 + Modules/CustomRolesHelper.cs | 3 + Modules/FixedUpdateCaller.cs | 7 ++ .../PlayerGameOptionsSender.cs | 1 + Modules/GameState.cs | 3 + Modules/OptionHolder.cs | 9 ++ Modules/Utils.cs | 1 + Patches/ChatCommandPatch.cs | 34 ++++-- Patches/ChatControlPatch.cs | 3 - Patches/CheckGameEndPatch.cs | 2 + Patches/PlayerJoinAndLeftPatch.cs | 2 +- Resources/Lang/en_US.json | 13 ++- Roles/Coven/Banshee.cs | 7 +- Roles/Neutral/Clerk.cs | 105 ++++++++++++++++++ 16 files changed, 177 insertions(+), 18 deletions(-) create mode 100644 Roles/Neutral/Clerk.cs diff --git a/CustomRoles.cs b/CustomRoles.cs index 408d0cb71..1f22880c5 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -289,6 +289,7 @@ public enum CustomRoles Bubble, Chemist, Cherokious, + Clerk, Collector, Curser, Deathknight, diff --git a/GameMode/HotPotato.cs b/GameMode/HotPotato.cs index c7afbb5a3..47a69877a 100644 --- a/GameMode/HotPotato.cs +++ b/GameMode/HotPotato.cs @@ -48,7 +48,7 @@ internal static class HotPotato .SetValueFormat(OptionFormat.Multiplier) .SetColor(new Color32(232, 205, 70, byte.MaxValue)); - HolderCanPassViaKillButton = new BooleanOptionItem(69_213_003, "HotPotato_HolderCanPassViaKillButton", false, TabGroup.GameSettings) + HolderCanPassViaKillButton = new BooleanOptionItem(69_213_003, "HotPotato_HolderCanPassViaKillButton", true, TabGroup.GameSettings) .SetGameMode(CustomGameMode.HotPotato) .SetColor(new Color32(232, 205, 70, byte.MaxValue)) .SetHeader(true); diff --git a/Main.cs b/Main.cs index 7503aa4f6..453d46e9b 100644 --- a/Main.cs +++ b/Main.cs @@ -573,6 +573,7 @@ public class Main : BasePlugin { CustomRoles.Seamstress, "#BFE44C" }, { CustomRoles.Spirit, "#26B652" }, { CustomRoles.Starspawn, "#4CE4B4" }, + { CustomRoles.Clerk, "#26B6A9" }, { CustomRoles.RoomRusher, "#ffab1b" }, { CustomRoles.SchrodingersCat, "#616161" }, { CustomRoles.Shifter, "#777777" }, @@ -1117,6 +1118,7 @@ public enum AdditionalWinners Seamstress = CustomRoles.Seamstress, Spirit = CustomRoles.Spirit, Starspawn = CustomRoles.Starspawn, + Clerk = CustomRoles.Clerk, RoomRusher = CustomRoles.RoomRusher, Impartial = CustomRoles.Impartial, Gaslighter = CustomRoles.Gaslighter, diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 9b6efb95d..4a763db6f 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -516,6 +516,7 @@ internal static class CustomRolesHelper CustomRoles.Postman => RoleTypes.Impostor, CustomRoles.Dealer => RoleTypes.Impostor, CustomRoles.Auditor => RoleTypes.Impostor, + CustomRoles.Clerk => RoleTypes.Impostor, CustomRoles.Seamstress => RoleTypes.Shapeshifter, CustomRoles.Spirit => RoleTypes.Shapeshifter, CustomRoles.Starspawn => RoleTypes.Impostor, @@ -889,6 +890,7 @@ internal static class CustomRolesHelper CustomRoles.Godfather when Options.GodfatherCancelVote.GetBool() => true, CustomRoles.Socialite when Socialite.CancelVote.GetBool() => true, CustomRoles.Negotiator when Negotiator.CancelVote.GetBool() => true, + CustomRoles.Clerk when Clerk.CancelVote.GetBool() => true, CustomRoles.President => true, @@ -1525,6 +1527,7 @@ internal static class CustomRolesHelper CustomRoles.Innocent => RoleOptionType.Neutral_Evil, CustomRoles.Curser => RoleOptionType.Neutral_Pariah, CustomRoles.Auditor => RoleOptionType.Neutral_Pariah, + CustomRoles.Clerk => RoleOptionType.Neutral_Pariah, CustomRoles.Magistrate => RoleOptionType.Neutral_Pariah, CustomRoles.Seamstress => RoleOptionType.Neutral_Pariah, CustomRoles.Spirit => RoleOptionType.Neutral_Pariah, diff --git a/Modules/FixedUpdateCaller.cs b/Modules/FixedUpdateCaller.cs index 8baa9364e..4bf8c6e4a 100644 --- a/Modules/FixedUpdateCaller.cs +++ b/Modules/FixedUpdateCaller.cs @@ -15,6 +15,7 @@ public static class FixedUpdateCaller private static int NonLowLoadPlayerIndex; private static long LastFileLoadTS; + private static long LastAutoMessageSendTS; // ReSharper disable once UnusedMember.Global public static void Postfix() @@ -48,6 +49,12 @@ public static class FixedUpdateCaller LastFileLoadTS = now; Options.LoadUserData(); } + + if (Options.EnableAutoMessage.GetBool() && now - LastAutoMessageSendTS > Options.AutoMessageSendInterval.GetInt()) + { + LastAutoMessageSendTS = now; + TemplateManager.SendTemplate("Notification", sendOption: Hazel.SendOption.None); + } } HudManager hudManager = FastDestroyableSingleton.Instance; diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index 9cfec7608..fe9f9585f 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -320,6 +320,7 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS Wizard.OnAnyoneApplyGameOptions(opt, player.PlayerId); Curser.OnAnyoneApplyGameOptions(opt, player.PlayerId); Auditor.OnAnyoneApplyGameOptions(opt, player.PlayerId); + Clerk.OnAnyoneApplyGameOptions(opt, player.PlayerId); if (Sprayer.LowerVisionList.Contains(player.PlayerId)) { diff --git a/Modules/GameState.cs b/Modules/GameState.cs index ef067fa93..3b9807948 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -68,6 +68,7 @@ public class PlayerState(byte playerId) WipedOut, OutOfOxygen, Retribution, + Taxes, // Natural Disasters Meteor, @@ -456,6 +457,8 @@ public class TaskState if (AmongUsClient.Instance.AmHost) { + Clerk.DidTaskThisRound.Add(player.PlayerId); + bool alive = player.IsAlive(); if (alive && Options.CurrentGameMode == CustomGameMode.Speedrun) diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index db92483c3..dc8f17863 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -729,6 +729,8 @@ public static class Options public static OptionItem AllCrewRolesHaveVanillaColor; public static OptionItem MessageRpcSizeLimit; public static OptionItem KickSlowJoiningPlayers; + public static OptionItem EnableAutoMessage; + public static OptionItem AutoMessageSendInterval; public static OptionItem DraftMaxRolesPerPlayer; public static OptionItem LargerRoleTextSize; public static OptionItem ShowTaskCountWhenAlive; @@ -1738,6 +1740,13 @@ public static class Options KickSlowJoiningPlayers = new BooleanOptionItem(19428, "KickSlowJoiningPlayers", false, TabGroup.SystemSettings) .SetHeader(true); + + EnableAutoMessage = new BooleanOptionItem(19429, "EnableAutoMessage", false, TabGroup.SystemSettings) + .SetHeader(true); + + AutoMessageSendInterval = new IntegerOptionItem(19430, "AutoMessageSendInterval", new(10, 300, 5), 60, TabGroup.SystemSettings) + .SetParent(EnableAutoMessage) + .SetValueFormat(OptionFormat.Seconds); RoleAssigningAlgorithm = new StringOptionItem(19409, "RoleAssigningAlgorithm", RoleAssigningAlgorithms, 4, TabGroup.SystemSettings, true) .SetHeader(true) diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 0d69d6951..e8dc17197 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -871,6 +871,7 @@ public static class Utils case CustomRoles.Postman: case CustomRoles.Dealer: case CustomRoles.Auditor: + case CustomRoles.Clerk: case CustomRoles.Magistrate: case CustomRoles.Seamstress: case CustomRoles.Spirit: diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 901870bcf..7d3706859 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -164,7 +165,7 @@ internal static class ChatCommands new(["target", "цель", "腹语者标记", "alvo"], "{id}", GetString("CommandDescription.Target"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, TargetCommand, true, true, [GetString("CommandArgs.Target.Id")]), new(["chat", "сообщение", "腹语者发送消息"], "{message}", GetString("CommandDescription.Chat"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, ChatCommand, true, true, [GetString("CommandArgs.Chat.Message")]), new(["check", "проверить", "检查", "veificar"], "{id} {role}", GetString("CommandDescription.Check"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, CheckCommand, true, true, [GetString("CommandArgs.Check.Id"), GetString("CommandArgs.Check.Role")]), - new(["ban", "kick", "бан", "кик", "забанить", "кикнуть", "封禁", "踢出", "banir", "expulsar"], "{id}", GetString("CommandDescription.Ban"), Command.UsageLevels.HostOrModerator, Command.UsageTimes.Always, BanKickCommand, true, false, [GetString("CommandArgs.Ban.Id")]), + new(["ban", "kick", "бан", "кик", "забанить", "кикнуть", "封禁", "踢出", "banir", "expulsar"], "{id} [reason]", GetString("CommandDescription.Ban"), Command.UsageLevels.HostOrModerator, Command.UsageTimes.Always, BanKickCommand, true, false, [GetString("CommandArgs.Ban.Id"), GetString("CommandArgs.Ban.Reason")]), new(["exe", "выкинуть", "驱逐", "executar"], "{id}", GetString("CommandDescription.Exe"), Command.UsageLevels.HostOrAdmin, Command.UsageTimes.Always, ExeCommand, true, false, [GetString("CommandArgs.Exe.Id")]), new(["kill", "убить", "击杀", "matar"], "{id}", GetString("CommandDescription.Kill"), Command.UsageLevels.Host, Command.UsageTimes.Always, KillCommand, true, false, [GetString("CommandArgs.Kill.Id")]), new(["colour", "color", "цвет", "更改颜色", "cor"], "{color}", GetString("CommandDescription.Colour"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, ColorCommand, true, false, [GetString("CommandArgs.Colour.Color")]), @@ -1304,6 +1305,8 @@ internal static class ChatCommands return; } + DraftResult = []; + byte[] allPlayerIds = Main.AllPlayerControls.Select(x => x.PlayerId).ToArray(); List allRoles = Enum.GetValues().Where(x => x < CustomRoles.NotAssigned && x.IsEnable() && !x.IsForOtherGameMode() && !CustomHnS.AllHnSRoles.Contains(x) && !x.IsVanilla() && x is not CustomRoles.GM and not CustomRoles.Konan).ToList(); @@ -2088,13 +2091,30 @@ internal static class ChatCommands return; } - // Kick the specified Player - AmongUsClient.Instance.KickPlayer(kickedPlayer.OwnerId, args[0] == "/ban"); - string kickedPlayerName = kickedPlayer.GetRealName(); - var textToSend = $"{kickedPlayerName} {GetString("KickCommandKicked")}"; - if (GameStates.IsInGame) textToSend += $"{GetString("KickCommandKickedRole")} {kickedPlayer.GetCustomRole().ToColoredString()}"; + try + { + string kickedPlayerName = kickedPlayer.GetRealName(); + var textToSend = $"{kickedPlayerName} {GetString("KickCommandKicked")}"; + if (GameStates.IsInGame) textToSend += $"{GetString("KickCommandKickedRole")} {kickedPlayer.GetCustomRole().ToColoredString()}"; + if (args.Length >= 3) textToSend += $"\n{GetString("KickCommandKickedReason")} {string.Join(' ', args[2..])}"; - Utils.SendMessage(textToSend, sendOption: GameStates.IsInGame ? SendOption.Reliable : SendOption.None); + Utils.SendMessage(textToSend, sendOption: GameStates.IsInGame ? SendOption.Reliable : SendOption.None); + + string modLogFilePath = $"{Main.DataPath}/EHR_DATA/ModLogs/{DateTime.Now:yyyy-MM-dd}.txt"; + + if (!File.Exists(modLogFilePath)) + { + string directoryName = Path.GetDirectoryName(modLogFilePath); + if (!string.IsNullOrEmpty(directoryName)) Directory.CreateDirectory(directoryName); + File.WriteAllText(modLogFilePath, "=== Moderation Log ===\n"); + } + + string logEntry = $"[{DateTime.Now:HH:mm:ss}] {player.GetRealName()} {(args[0] == "/ban" ? "banned" : "kicked")} {kickedPlayerName} [{kickedPlayer.FriendCode}|{kickedPlayer.GetClient().GetHashedPuid()}] for {(args.Length >= 3 ? string.Join(' ', args[2..]) : "[no reason provided]")}\n"; + File.AppendAllText(modLogFilePath, logEntry); + } + catch (Exception e) { Utils.ThrowException(e); } + + AmongUsClient.Instance.KickPlayer(kickedPlayer.OwnerId, args[0] == "/ban"); } private static void CheckCommand(PlayerControl player, string text, string[] args) diff --git a/Patches/ChatControlPatch.cs b/Patches/ChatControlPatch.cs index 5c469d713..3493e59f8 100644 --- a/Patches/ChatControlPatch.cs +++ b/Patches/ChatControlPatch.cs @@ -207,9 +207,6 @@ public static class ChatManager case CustomGameMode.FFA when GameStates.InGame && !message.StartsWith('/'): FreeForAll.UpdateLastChatMessage(player.GetRealName(), message); break; - case CustomGameMode.Standard when GameStates.InGame && operate != 1 && Banshee.On: - Banshee.OnReceiveChat(); - break; } } diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index 6ae72f7bf..5f9b52913 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -236,6 +236,7 @@ internal static class GameEndChecker case CustomRoles.NoteKiller when !NoteKiller.CountsAsNeutralKiller && NoteKiller.Kills >= NoteKiller.NumKillsNeededToWin: case CustomRoles.NecroGuesser when (roleBase as NecroGuesser).GuessedPlayers >= NecroGuesser.NumGuessesToWin.GetInt(): case CustomRoles.RoomRusher when (roleBase as RoomRusher).Won: + case CustomRoles.Clerk when WinnerTeam != CustomWinner.Crewmate || reason == GameOverReason.CrewmatesByTask: case CustomRoles.Auditor when WinnerTeam != CustomWinner.Crewmate: case CustomRoles.Magistrate when WinnerTeam != CustomWinner.Crewmate: case CustomRoles.Seamstress when WinnerTeam != CustomWinner.Crewmate: @@ -461,6 +462,7 @@ internal static class GameEndChecker case CustomRoles.NecroGuesser when ((NecroGuesser)state.Role).GuessedPlayers >= NecroGuesser.NumGuessesToWin.GetInt(): case CustomRoles.RoomRusher when ((RoomRusher)state.Role).Won: case CustomRoles.Auditor: + case CustomRoles.Clerk: case CustomRoles.Magistrate: case CustomRoles.Seamstress: case CustomRoles.Spirit: diff --git a/Patches/PlayerJoinAndLeftPatch.cs b/Patches/PlayerJoinAndLeftPatch.cs index 309397db8..ee3c73f75 100644 --- a/Patches/PlayerJoinAndLeftPatch.cs +++ b/Patches/PlayerJoinAndLeftPatch.cs @@ -149,7 +149,7 @@ internal static class OnGameJoinedPatch float timer; if (nextGM != CustomGameMode.All) timer = 0f; - else if (Options.AutoGMPollCommandAfterJoin.GetBool()) timer = Options.AutoGMPollCommandCooldown.GetInt(); + else if (Options.AutoGMPollCommandAfterJoin.GetBool()) timer = Options.AutoGMPollCommandCooldown.GetInt() - 10; else if (Main.AutoStart.Value) timer = (Options.MinWaitAutoStart.GetFloat() * 60) - 65; else timer = 30f; diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index a3c8ee741..2da947217 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -1884,6 +1884,7 @@ "KickCommandKickMod": "You are not permitted to kick other moderators.", "KickCommandKicked": "was kicked from the game.", "KickCommandKickedRole": "Their role was", + "KickCommandKickedReason": "Reason:", "ModeratorTag": "<#8000ff>Moderator", "VIPTag": "<#ffff00>VIP", "AdminTag": "<#ff0000>Admin", @@ -1958,6 +1959,7 @@ "DeathReason.Mauled": "Mauled", "DeathReason.WipedOut": "Wiped Out", "DeathReason.Retribution": "Retribution", + "DeathReason.Taxes": "Taxes", "ND_DRLaughMessage.Meteor": "Meteor.... I hardly knew her!", "ND_DRLaughMessage.Lava": "Lava is not the best place to swim, huh?", "ND_DRLaughMessage.Tornado": "Out for a spin? I hope this tornado didn't make you dizzy!", @@ -2514,6 +2516,7 @@ "CommandArgs.Check.Id": "The player's ID to check", "CommandArgs.Check.Role": "The role or add-on to check for", "CommandArgs.Ban.Id": "The player's ID to ban or kick", + "CommandArgs.Ban.Reason": "The reason for the punishment", "CommandArgs.Exe.Id": "The player's ID to execute", "CommandArgs.Kill.Id": "The player's ID to kill", "CommandArgs.Colour.Color": "The color to set (ex. red, blue, yellow)", @@ -4470,12 +4473,14 @@ "GuessBackstabber": "You cannot guess a revealed Backstabber.", "HideResults": "Hide Results", "ShowResults": "Show Results", + "EnableAutoMessage": "Enable Auto-Message (sends the template with the 'Notification' tag on a given interval in lobby)", + "AutoMessageSendInterval": "The Auto-Message Is Sent Every", "Wiper": "Wiper", "WiperInfo": "Wipe Out Rooms", "WiperInfoLong": "(Impostors):\\nAs the Wiper, you can wipe out a room by using pet/vanish. This will kill everyone in that room. (Even other impostors, depending on the settings.)\\nYou cannot kill normally.\\nYou cannot use your ability during sabotages.", "Wiper.KillOtherImpostors": "Wiping Out a Room Kills Other <#ff1919>Impostors", "Wiper.CannotUseAbilityDuringSabotage": "You cannot use your ability during sabotages", - "Wiper.MustBeInRoomToUseAbility": "You must be in a generic room to use your ability", + "Wiper.MustBeInRoomToUseAbility": "You must be in a room to use your ability\\nNot in the spawn area", "Wiper.CurrentRoom": "<#00ffa5>Current Room: {0}", "Curser": "Curser", "CurserInfo": "Make Others' Life Worse", @@ -9251,6 +9256,12 @@ "SleepInfo": "You Sleep During Lights Out", "SleepInfoLong": "(Add-ons):\\nYou sleep during lights out. You can't see anything, and you cannot move. You can only wake up when the lights are back on. If someone with the Glow add-on comes near you, you fully wake up and lose this add-on.", "Prefix.Sleep": "Sleeping", + "Clerk": "Clerk", + "ClerkInfo": "Check Players' Taxes", + "ClerkInfoLong": "(Neutrals):\\nAs the Clerk, you may vote for someone in a meeting to check their taxes. If they've completed a task in the last round (or finished them all, or they have none), they get a tax refund (speed & vision boost for the next round). Otherwise, they die.", + "Clerk.CanUseAbilityOnFirstMeeting": "Can Use Ability on First Meeting", + "Clerk.DidTax": "{0} did their taxes. They got a refund!", + "Clerk.Killed": "{0} was cought not doing their taxes (tasks). They were killed!", "Decryptor": "Decryptor", "DecryptorInfo": "Get Letters From Everyone's Roles", "DecryptorInfoLong": "(Crewmates):\\nAs the Decryptor, each X tasks you complete (X is set by the host), you get to know one more letter from everyone's roles. You can see these characters below players' names. The letters are not in order and each letter only appears once for every occurance in the role name. Settings determine whether you can guess.", diff --git a/Roles/Coven/Banshee.cs b/Roles/Coven/Banshee.cs index 0a9843797..1f2c401ab 100644 --- a/Roles/Coven/Banshee.cs +++ b/Roles/Coven/Banshee.cs @@ -111,11 +111,8 @@ public class Banshee : Coven } } - public static void OnReceiveChat() + public override void OnReportDeadBody() { - PlayerControl[] screechedPlayers = Instances.SelectMany(x => x.ScreechedPlayers).Distinct().ToValidPlayers().ToArray(); - if (screechedPlayers.Length == 0) return; - - LateTask.New(() => ChatManager.ClearChat(screechedPlayers), Utils.CalculatePingDelay()); + LateTask.New(() => Instances.SelectMany(x => x.ScreechedPlayers).Distinct().ToValidPlayers().Do(x => x.SetChatVisible(false)), 4f, "Set Chat Hidden For Banshee Victims"); } } \ No newline at end of file diff --git a/Roles/Neutral/Clerk.cs b/Roles/Neutral/Clerk.cs new file mode 100644 index 000000000..4b64c6bfa --- /dev/null +++ b/Roles/Neutral/Clerk.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using AmongUs.GameOptions; +using EHR.Crewmate; +using Hazel; + +namespace EHR.Neutral; + +public class Clerk : RoleBase +{ + public static bool On; + + public override bool IsEnable => On; + + private static OptionItem AbilityUseLimit; + private static OptionItem CanUseAbilityOnFirstMeeting; + private static OptionItem IncreasedSpeed; + private static OptionItem IncreasedVision; + public static OptionItem CancelVote; + + public static HashSet DidTaskThisRound = []; + public static HashSet HasBoost = []; + + public override void SetupCustomOption() + { + StartSetup(654800) + .AutoSetupOption(ref AbilityUseLimit, 5, new IntegerValueRule(1, 30, 1), OptionFormat.Times) + .AutoSetupOption(ref CanUseAbilityOnFirstMeeting, false) + .AutoSetupOption(ref IncreasedSpeed, 1.8f, new FloatValueRule(0.1f, 3f, 0.1f), OptionFormat.Multiplier) + .AutoSetupOption(ref IncreasedVision, 1f, new FloatValueRule(0.1f, 1.5f, 0.1f), OptionFormat.Multiplier) + .CreateVoteCancellingUseSetting(ref CancelVote); + } + + public override void Init() + { + On = false; + DidTaskThisRound = []; + HasBoost = []; + } + + public override void Add(byte playerId) + { + On = true; + playerId.SetAbilityUseLimit(AbilityUseLimit.GetInt()); + } + + public override bool OnVote(PlayerControl voter, PlayerControl target) + { + if (Starspawn.IsDayBreak) return false; + if (Main.DontCancelVoteList.Contains(voter.PlayerId)) return false; + if (MeetingStates.FirstMeeting && !CanUseAbilityOnFirstMeeting.GetBool()) return false; + if (voter.GetAbilityUseLimit() < 1) return false; + if (target.PlayerId == voter.PlayerId) return false; + + voter.RpcRemoveAbilityUse(); + + TaskState ts = target.GetTaskState(); + + if (DidTaskThisRound.Contains(target.PlayerId) || !ts.HasTasks || ts.IsTaskFinished) + { + HasBoost.Add(target.PlayerId); + Utils.SendMessage(string.Format(Translator.GetString("Clerk.DidTax"), target.PlayerId.ColoredPlayerName()), voter.PlayerId, CustomRoles.Clerk.ToColoredString()); + } + else if (!target.Is(CustomRoles.Pestilence)) + { + target.SetRealKiller(voter); + PlayerState state = Main.PlayerStates[target.PlayerId]; + state.deathReason = PlayerState.DeathReason.Taxes; + state.SetDead(); + Medic.IsDead(target); + target.RpcExileV2(); + Utils.AfterPlayerDeathTasks(target, true); + Utils.SendMessage(string.Format(Translator.GetString("Clerk.Killed"), target.PlayerId.ColoredPlayerName()), title: CustomRoles.Clerk.ToColoredString()); + } + + Main.DontCancelVoteList.Add(voter.PlayerId); + return true; + } + + public override void OnMeetingShapeshift(PlayerControl shapeshifter, PlayerControl target) + { + OnVote(shapeshifter, target); + } + + public override void AfterMeetingTasks() + { + DidTaskThisRound = []; + } + + public override void OnReportDeadBody() + { + HasBoost.Do(x => Main.AllPlayerSpeed[x] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod)); + HasBoost = []; + } + + public static void OnAnyoneApplyGameOptions(IGameOptions opt, byte id) + { + if (HasBoost.Contains(id)) + { + Main.AllPlayerSpeed[id] = IncreasedSpeed.GetFloat(); + float increasedVision = IncreasedVision.GetFloat(); + opt.SetFloat(FloatOptionNames.CrewLightMod, increasedVision); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, increasedVision); + } + } +} \ No newline at end of file From dd1f9d7119a6b9daac242bf45693d04dfa406eeb Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Fri, 12 Sep 2025 10:56:15 +0200 Subject: [PATCH 12/24] a --- Resources/Lang/en_US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 2da947217..ed079dc98 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -9258,7 +9258,7 @@ "Prefix.Sleep": "Sleeping", "Clerk": "Clerk", "ClerkInfo": "Check Players' Taxes", - "ClerkInfoLong": "(Neutrals):\\nAs the Clerk, you may vote for someone in a meeting to check their taxes. If they've completed a task in the last round (or finished them all, or they have none), they get a tax refund (speed & vision boost for the next round). Otherwise, they die.", + "ClerkInfoLong": "(Neutrals):\\nAs the Clerk, you may vote for someone in a meeting to check their taxes. If they've completed a task in the last round (or finished them all, or they have none), they get a tax refund (speed & vision boost for the next round). Otherwise, they die.\\n\\nYou win if crewmates lose. Or if the game ends due to a task win (everyone did their taxes)!", "Clerk.CanUseAbilityOnFirstMeeting": "Can Use Ability on First Meeting", "Clerk.DidTax": "{0} did their taxes. They got a refund!", "Clerk.Killed": "{0} was cought not doing their taxes (tasks). They were killed!", From da09d5af1f69a01a57a03b4cad5c23cd3285a197 Mon Sep 17 00:00:00 2001 From: HyperAtill Date: Fri, 12 Sep 2025 17:01:49 +0300 Subject: [PATCH 13/24] unban --- Resources/Config/EACList.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/Resources/Config/EACList.txt b/Resources/Config/EACList.txt index fcf5f3089..e8d41a9e2 100644 --- a/Resources/Config/EACList.txt +++ b/Resources/Config/EACList.txt @@ -125,7 +125,6 @@ tidytrice#2024,Alisa?(either this or above one) Foamcrisp#5124, e5be77e02, Barbie (aka princess, broadbarn alt) rosefiscal#4338, Bagelz (suspected alt of broadbarn or tidytrice) driedbreak#2568, 2f9986d9f, ASHHHOLE (toxic) -wheywaking#9984, 239d9606d, name not known (toxic) ## Racist/Homophobic onwardhome#3591, MentoMan From e957b533d38c9b9ea7ade4cc206c3876d70b09b4 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Fri, 12 Sep 2025 16:28:17 +0200 Subject: [PATCH 14/24] quiz time decrease --- GameMode/Quiz.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/GameMode/Quiz.cs b/GameMode/Quiz.cs index 14c4e5335..ea3ef078f 100644 --- a/GameMode/Quiz.cs +++ b/GameMode/Quiz.cs @@ -87,6 +87,7 @@ public static class Quiz private static Difficulty CurrentDifficulty; private static int Round; private static int QuestionsAsked; + private static int NumAllCorrectAnswers; private static long FFAEndTS; private static bool NoSuffix; public static bool AllowKills; @@ -252,6 +253,7 @@ public static class Quiz CurrentDifficulty = Difficulty.Easy; Round = 0; QuestionsAsked = 0; + NumAllCorrectAnswers = 0; FFAEndTS = 0; AllowKills = false; NoSuffix = true; @@ -356,6 +358,7 @@ public static class Quiz time += CurrentQuestion.Question.Length / 20; time += CurrentQuestion.Answers.Sum(x => x.Length / 15); if (Main.CurrentMap is MapNames.Skeld or MapNames.Dleks) time -= 3; + time -= NumAllCorrectAnswers; QuestionTimeLimitEndTS = Utils.TimeStamp + time; PlayerControl[] aapc = Main.AllAlivePlayerControls; @@ -448,6 +451,7 @@ public static class Quiz PlayerControl[] aapc = Main.AllAlivePlayerControls; SystemTypes correctRoom = UsedRooms[Main.CurrentMap][(char)('A' + CurrentQuestion.CorrectAnswerIndex)]; DyingPlayers = aapc.Select(x => (ID: x.PlayerId, Room: x.GetPlainShipRoom())).Where(x => correctRoom == SystemTypes.Outside ? x.Room != null : x.Room == null || x.Room.RoomId != correctRoom).Select(x => x.ID).ToList(); + if (DyingPlayers.Count == 0) NumAllCorrectAnswers++; bool everyoneWasWrong = DyingPlayers.Count == aapc.Length; if (!everyoneWasWrong) NumCorrectAnswers.IntersectBy(aapc.Select(x => x.PlayerId), x => x.Key).DoIf(x => !DyingPlayers.Contains(x.Key), x => x.Value[CurrentDifficulty][Round]++); Logger.Info($"{(everyoneWasWrong ? "Everyone" : "Players who")} got the question wrong: {string.Join(", ", DyingPlayers.Select(x => Main.AllPlayerNames.GetValueOrDefault(x, $"Someone (ID {x})")))}", "Quiz"); From 2c87c43b3c02e6fdc2f141a9cfb582756049084b Mon Sep 17 00:00:00 2001 From: PEPPERcula Date: Sun, 14 Sep 2025 00:43:26 -0700 Subject: [PATCH 15/24] Add more Admin perms for players (#355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update ChatCommandPatch.cs * Update ChatCommandPatch.cs * Update ClientPatch.cs * Update ControlPatch.cs * Update en_US.json * Update en_US.json * Update en_US.json * Update README.md * Update README.md * Update README.md * add parentheses * remove unnecessary check * again * this made 0 sense this would have prevented the host from banning players if they haven't added themselves as an admin 💀 * fix this --------- Co-authored-by: Gurge44 <90392754+Gurge44@users.noreply.github.com> --- Patches/ChatCommandPatch.cs | 18 +++++++++++------- Patches/ClientPatch.cs | 3 +++ Patches/ControlPatch.cs | 21 ++++++++++++++------- README.md | 31 +++++++++++++++++-------------- Resources/Lang/en_US.json | 5 +++-- 5 files changed, 48 insertions(+), 30 deletions(-) diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 7d3706859..069b23b9f 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -21,7 +21,6 @@ using static EHR.Translator; // ReSharper disable InconsistentNaming - namespace EHR; internal class Command(string[] commandForms, string arguments, string description, Command.UsageLevels usageLevel, Command.UsageTimes usageTime, Action action, bool isCanceled, bool alwaysHidden, string[] argsDescriptions = null) @@ -149,7 +148,7 @@ internal static class ChatCommands new(["deletemod", "убрмодера", "удмодера", "убратьмодера", "удалитьмодера", "移除协管", "moderador-remover"], "{id}", GetString("CommandDescription.DeleteMod"), Command.UsageLevels.Host, Command.UsageTimes.Always, DeleteModCommand, true, false, [GetString("CommandArgs.DeleteMod.Id")]), new(["combo", "комбо", "设置不会同时出现的职业", "combinação", "combinar"], "{mode} {role} {addon} [all]", GetString("CommandDescription.Combo"), Command.UsageLevels.Host, Command.UsageTimes.Always, ComboCommand, true, false, [GetString("CommandArgs.Combo.Mode"), GetString("CommandArgs.Combo.Role"), GetString("CommandArgs.Combo.Addon"), GetString("CommandArgs.Combo.All")]), new(["eff", "effect", "эффект", "效果", "efeito"], "{effect}", GetString("CommandDescription.Effect"), Command.UsageLevels.Host, Command.UsageTimes.InGame, EffectCommand, true, false, [GetString("CommandArgs.Effect.Effect")]), - new(["afkexempt", "освафк", "афкосв", "挂机检测器不会检测", "afk-isentar"], "{id}", GetString("CommandDescription.AFKExempt"), Command.UsageLevels.Host, Command.UsageTimes.Always, AFKExemptCommand, true, false, [GetString("CommandArgs.AFKExempt.Id")]), + new(["afkexempt", "освафк", "афкосв", "挂机检测器不会检测", "afk-isentar"], "{id}", GetString("CommandDescription.AFKExempt"), Command.UsageLevels.HostOrAdmin, Command.UsageTimes.Always, AFKExemptCommand, true, false, [GetString("CommandArgs.AFKExempt.Id")]), new(["m", "myrole", "м", "мояроль", "我的职业", "minhafunção"], "", GetString("CommandDescription.MyRole"), Command.UsageLevels.Everyone, Command.UsageTimes.InGame, MyRoleCommand, true, false), new(["tpout", "тпаут", "传送出"], "", GetString("CommandDescription.TPOut"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, TPOutCommand, true, false), new(["tpin", "тпин", "传送进"], "", GetString("CommandDescription.TPIn"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, TPInCommand, true, false), @@ -188,11 +187,11 @@ internal static class ChatCommands new(["os", "optionset", "шансроли", "设置职业生成概率"], "{chance} {role}", GetString("CommandDescription.OS"), Command.UsageLevels.Host, Command.UsageTimes.InLobby, OSCommand, true, false, [GetString("CommandArgs.OS.Chance"), GetString("CommandArgs.OS.Role")]), new(["negotiation", "neg", "наказание", "谈判方式", "negociar", "negociação"], "{number}", GetString("CommandDescription.Negotiation"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, NegotiationCommand, true, false, [GetString("CommandArgs.Negotiation.Number")]), new(["mute", "замутить", "мут", "禁言", "mutar", "silenciar"], "{id} [duration]", GetString("CommandDescription.Mute"), Command.UsageLevels.HostOrModerator, Command.UsageTimes.AfterDeathOrLobby, MuteCommand, true, false, [GetString("CommandArgs.Mute.Id"), GetString("CommandArgs.Mute.Duration")]), - new(["unmute", "размутить", "размут", "解禁", "desmutar", "desilenciar"], "{id}", GetString("CommandDescription.Unmute"), Command.UsageLevels.Host, Command.UsageTimes.Always, UnmuteCommand, true, false, [GetString("CommandArgs.Unmute.Id")]), + new(["unmute", "размутить", "размут", "解禁", "desmutar", "desilenciar"], "{id}", GetString("CommandDescription.Unmute"), Command.UsageLevels.HostOrAdmin, Command.UsageTimes.Always, UnmuteCommand, true, false, [GetString("CommandArgs.Unmute.Id")]), new(["draftstart", "ds", "драфтстарт", "启用草稿", "todosescolhem-iniciar"], "", GetString("CommandDescription.DraftStart"), Command.UsageLevels.HostOrModerator, Command.UsageTimes.InLobby, DraftStartCommand, true, false), new(["dd", "draftdesc", "draftdescription", "драфтописание", "драфтопис", "草稿描述", "todosescolhem-descricao"], "{index}", GetString("CommandDescription.DraftDescription"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, DraftDescriptionCommand, false, false, [GetString("CommandArgs.DraftDescription.Index")]), new(["draft", "драфт", "选择草稿", "todosescolhem-escolher"], "{number}", GetString("CommandDescription.Draft"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, DraftCommand, false, false, [GetString("CommandArgs.Draft.Number")]), - new(["rc", "readycheck", "проверитьготовность", "准备检测", "verificação-de-prontidão"], "", GetString("CommandDescription.ReadyCheck"), Command.UsageLevels.Host, Command.UsageTimes.InLobby, ReadyCheckCommand, true, false), + new(["rc", "readycheck", "проверитьготовность", "准备检测", "verificação-de-prontidão"], "", GetString("CommandDescription.ReadyCheck"), Command.UsageLevels.HostOrAdmin, Command.UsageTimes.InLobby, ReadyCheckCommand, true, false), new(["ready", "готов", "准备", "pronto"], "", GetString("CommandDescription.Ready"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, ReadyCommand, true, false), new(["enableallroles", "вклвсероли", "启用所有职业", "habilitar-todas-as-funções"], "", GetString("CommandDescription.EnableAllRoles"), Command.UsageLevels.Host, Command.UsageTimes.InLobby, EnableAllRolesCommand, true, false), new(["achievements", "достижения", "成就", "conquistas"], "", GetString("CommandDescription.Achievements"), Command.UsageLevels.Modded, Command.UsageTimes.Always, AchievementsCommand, true, false), @@ -1248,7 +1247,7 @@ internal static class ChatCommands { if (!AmongUsClient.Instance.AmHost) { - RequestCommandProcessingFromHost(nameof(ReadyCheckCommand), text); + RequestCommandProcessingFromHost(nameof(ReadyCheckCommand), text, adminCommand: true); return; } @@ -1445,7 +1444,7 @@ internal static class ChatCommands { if (!AmongUsClient.Instance.AmHost) { - RequestCommandProcessingFromHost(nameof(UnmuteCommand), text); + RequestCommandProcessingFromHost(nameof(UnmuteCommand), text, adminCommand: true); return; } @@ -1454,6 +1453,7 @@ internal static class ChatCommands MutedPlayers.Remove(id); Utils.SendMessage("\n", player.PlayerId, string.Format(GetString("PlayerUnmuted"), id.ColoredPlayerName())); Utils.SendMessage("\n", id, string.Format(GetString("YouUnmuted"), player.PlayerId.ColoredPlayerName())); + if (!player.IsHost()) Utils.SendMessage("\n", 0, string.Format(GetString("AdminUnmuted"), player.PlayerId.ColoredPlayerName(), id.ColoredPlayerName())); } private static void NegotiationCommand(PlayerControl player, string text, string[] args) @@ -2437,7 +2437,7 @@ internal static class ChatCommands { if (!AmongUsClient.Instance.AmHost) { - RequestCommandProcessingFromHost(nameof(AFKExemptCommand), text); + RequestCommandProcessingFromHost(nameof(AFKExemptCommand), text, adminCommand: true); return; } @@ -3825,3 +3825,7 @@ internal static class RpcSendChatPatch return false; } } + + + + diff --git a/Patches/ClientPatch.cs b/Patches/ClientPatch.cs index 8db8d1279..cc3c5ea4a 100644 --- a/Patches/ClientPatch.cs +++ b/Patches/ClientPatch.cs @@ -211,3 +211,6 @@ internal static class AuthTimeoutPatch #endif } + + + diff --git a/Patches/ControlPatch.cs b/Patches/ControlPatch.cs index e667394e2..33b325b24 100644 --- a/Patches/ControlPatch.cs +++ b/Patches/ControlPatch.cs @@ -55,7 +55,12 @@ internal static class ControllerManagerUpdatePatch else InGameRoleInfoMenu.Hide(); - if (Input.GetKeyDown(KeyCode.F11)) + bool admin = ChatCommands.IsPlayerAdmin(PlayerControl.LocalPlayer.FriendCode); + + if (KeysDown(KeyCode.Return, KeyCode.C, KeyCode.LeftShift) && admin) + HudManager.Instance.Chat.SetVisible(true); + + if (KeysDown(KeyCode.F11, KeyCode.LeftAlt)) { ResolutionIndex++; if (ResolutionIndex >= Resolutions.Length) ResolutionIndex = 0; @@ -84,12 +89,10 @@ internal static class ControllerManagerUpdatePatch Utils.DumpLog(); } - if (KeysDown(KeyCode.LeftAlt, KeyCode.C) && !Input.GetKey(KeyCode.LeftShift) && !GameStates.IsNotJoined) Utils.CopyCurrentSettings(); + if (KeysDown(KeyCode.LeftAlt, KeyCode.C) && !Input.GetKey(KeyCode.LeftShift) && !GameStates.IsNotJoined) + Utils.CopyCurrentSettings(); - if (!AmongUsClient.Instance.AmHost) return; - - if (KeysDown(KeyCode.Return, KeyCode.C, KeyCode.LeftShift)) - HudManager.Instance.Chat.SetVisible(true); + if (!AmongUsClient.Instance.AmHost && !admin) return; if (KeysDown(KeyCode.Return, KeyCode.L, KeyCode.LeftShift) && GameStates.IsInGame) { @@ -97,6 +100,8 @@ internal static class ControllerManagerUpdatePatch GameEndChecker.CheckCustomEndCriteria(); } + if (!AmongUsClient.Instance.AmHost) return; + if (KeysDown(KeyCode.Return, KeyCode.C, KeyCode.LeftShift, KeyCode.LeftControl) && GameStates.IsInGame) Utils.SetChatVisibleForAll(); @@ -447,4 +452,6 @@ public static class InGameRoleInfoMenu Menu?.SetActive(false); } } -} \ No newline at end of file + +} + diff --git a/README.md b/README.md index 3c68840b2..0f5ed8e12 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Share the download link with your friends: [bit.ly/downloadehr](https://bit.ly/d ## Want to become a beta tester? Join our Discord server and download the latest test build from the #test-builds channel. -You do **NOT** need to pay, boost the server, donate, be a VIP, contributor or anything like that. +You do **NOT** need to pay, boost the server, donate, be a VIP, Contributor or anything like that. In this mod and its discord server, everything is free. We don't want your money, we want your feedback. You can also report bugs and suggest features in the #bug-reports and #suggestions channels. @@ -66,9 +66,9 @@ You can also report bugs and suggest features in the #bug-reports and #suggestio ### Visit this website to find role descriptions: [Role description finder](https://gurge44.pythonanywhere.com/) -#### Over 400 roles and add-ons, 12 custom game modes, and a custom team assigner application, making EHR the most extensive mod for Among Us. And it's all free, and only the host needs to have the mod installed for the entire lobby to play! +#### Over 400+ roles and add-ons, 13 custom game modes, and a Custom Team Assigner application, making EHR the most extensive mod for Among Us. And it's all free, and only the host needs to have the mod installed for the entire lobby to play! -## List of Roles (excluding (Custom) Hide and Seek roles and vanilla roles) +## List of Roles (excluding (Custom) Hide And Seek roles and Vanilla roles) | Crewmates | Impostors | Neutrals | Coven | Add-ons | |-------------------|-----------------|-------------------|---------------|----------------| @@ -101,7 +101,7 @@ You can also report bugs and suggest features in the #bug-reports and #suggestio | Dad | Disperser | The Glitch | | Diseased | | Demolitionist | Duellist | God | | Dynamo | | Deputy | Dazzler | Hater | | Disregarded | -| Detective | Echo | Head Hunter | | Double Shot | +| Forensic | Echo | Head Hunter | | Double Shot | | Detour | Escapist | Hex Master | | Egoist | | Dictator | Evil Eraser | Hookshot | | Energetic | | Doctor | Evil Guesser | Pulse | | Entranced | @@ -204,7 +204,7 @@ You can also report bugs and suggest features in the #bug-reports and #suggestio | | | | | | | 129 | 98 | 107 | 17 | 95 | -## VIP and Moderator perks +## VIP, Moderator, and Admin perks :heavy_check_mark: Can always use the command
:x: Can never use the command
@@ -249,7 +249,7 @@ Source language: English - /up was reworked to /setrole, allowing you to set anyone's role for the next game, not only yours. - You can fully gain access to /setrole by just hosting EHR lobbies weekly or uploading EHR content to YouTube, but everyone can use /setrole to set their own role without any requirements (essentially /up). -- EHR has a custom team assigner application, which is a unique feature that no other mod has. +- EHR has a Custom Team Assigner application, which is a unique feature that no other mod has. - You can force or ban certain role/add-on combinations directly in the game using /combo. - Not sure how to use commands? Use /help to see a list of all available commands. - EHR helps the host use commands by directly displaying their short descriptions, arguments, descriptions for all @@ -257,7 +257,7 @@ Source language: English - You can also press TAB to autocomplete commands. - Some hints and tips are displayed during the loading screen and ejection animations, which can help you discover new features. -- EHR has 12 unique custom game modes, which can be played with any number of players, some even alone. +- EHR has 13 unique custom game modes, which can be played with any number of players, some even alone. - You can make lobbies with more than 15 players on modded servers, just type /cs players 100 - for example - to set the player limit to 100. - I, the main developer of EHR, care about user feedback and suggestions, and I try to implement them as much as @@ -293,7 +293,7 @@ Source language: English | /deletemod | Removes the specified player from the list of moderators | 🔸 **id** – The player's ID to remove from the list of moderators
| :yellow_circle: Host | :purple_square: Always | :x: | | /combo | Set roles and add-ons that always or never spawn together | 🔸 **mode** – add/ban/remove/allow (leave blank to view all combos)
🔸 **role** – The name of the main role in 1 word
🔸 **addon** – The name of the add-on in 1 word
🔹 **all** – Whether to apply to all presets (all/[blank])
| :yellow_circle: Host | :purple_square: Always | :x: | | /eff
/effect | Apply a Randomizer effect to yourself | 🔸 **effect** – The name of the effect to apply
| :yellow_circle: Host | :white_large_square: In Game | :x: | -| /afkexempt | Exempt a player from being checked by the AFK detector | 🔸 **id** – The player's ID to exempt from AFK detection
| :yellow_circle: Host | :purple_square: Always | :x: | +| /afkexempt | Exempt a player from being checked by the AFK detector | 🔸 **id** – The player's ID to exempt from AFK detection
| :white_circle: Host And Admins | :purple_square: Always | :x: | | /m
/myrole | View the description and settings of your role | – | :purple_circle: Everyone | :white_large_square: In Game | :x: | | /tpout | Teleport out of the lobby | – | :purple_circle: Everyone | :green_square: In Lobby | :x: | | /tpin | Teleport into the lobby | – | :purple_circle: Everyone | :green_square: In Lobby | :x: | @@ -332,11 +332,11 @@ Source language: English | /os
/optionset | Set the spawn chance of a role | 🔸 **chance** – The spawn chance for the role
🔸 **role** – The role to set the spawn chance for
| :yellow_circle: Host | :green_square: In Lobby | :x: | | /negotiation
/neg | Pick your negotiation method when asked by the Negotiator | 🔸 **number** – The index of the picked negotiation method (0/1/2/3)
| :purple_circle: Everyone | :yellow_square: In Meetings | :x: | | /mute | Mute someone. Muted players can't chat. Everyone is unmuted on game start. | 🔸 **id** – The player's ID to mute
🔹 **duration** – The duration in seconds (leaving blank => 60s)
| :red_circle: Host, Moderators, And Admins | :brown_square: After Death And In Lobby | :x: | -| /unmute | Unmute someone. | 🔸 **id** – The player's ID to unmute
| :yellow_circle: Host | :purple_square: Always | :x: | +| /unmute | Unmute someone. | 🔸 **id** – The player's ID to unmute
| :white_circle: Host And Admins | :purple_square: Always | :x: | | /draftstart
/ds | Start drafting, where players may choose their roles for the next game | – | :red_circle: Host, Moderators, And Admins | :green_square: In Lobby | :x: | | /dd
/draftdesc
/draftdescription | View the description of one of the roles you got in the draft. | 🔸 **index** – The number in the drafting message of the role to view the description of
| :purple_circle: Everyone | :green_square: In Lobby | :x: | | /draft | Choose a role in the draft | 🔸 **number** – The number of the role to choose
| :purple_circle: Everyone | :green_square: In Lobby | :x: | -| /rc
/readycheck | Start a ready check (check who is AFK) (results displayed after 30s) | – | :yellow_circle: Host | :green_square: In Lobby | :x: | +| /rc
/readycheck | Start a ready check (check who is AFK) (results displayed after 30s) | – | :white_circle: Host And Admins | :green_square: In Lobby | :x: | | /ready | Mark yourself as ready | – | :purple_circle: Everyone | :green_square: In Lobby | :x: | | /enableallroles | Enables all roles and add-ons | – | :yellow_circle: Host | :green_square: In Lobby | :x: | | /achievements | See which achievements you have unlocked and which you haven't | – | :green_circle: Modded Clients | :purple_square: Always | :x: | @@ -384,14 +384,14 @@ Source language: English ### :star: [Town Of Host Re-Edited](https://github.com/Loonie-Toons/) : > -> - Our repo is forked from TOHRE (from a long time ago) +> - Our repository is forked from TOHRE (from a long time ago) > - Ported PhantomRolePatch.cs (partially broken) > ### :star: [Town Of Host Edited / Town Of Next](https://github.com/KARPED1EM/TownOfHostEdited) : > -> - Our repo is a continuation of Town Of Host Edited +> - Our repository is a continuation of Town Of Host Edited > - Town Of Next reference: Modify character limit for chat messages > - Town Of Next reference: Modify and apply cool animations on Main Menu > @@ -399,7 +399,7 @@ Source language: English ### :star: [TOH](https://github.com/tukasa0001/TownOfHost) : > -> - Our repo is based initially on TOH +> - Our repository is based initially on TOH > ### :star: [TOHY](https://github.com/Yumenopai/TownOfHost_Y) : @@ -515,7 +515,7 @@ Source language: English - PH_Gaming - TommyXL - Drakos -- Peppercula +- PEPPERcula ##### Special Thanks: @@ -667,3 +667,6 @@ If you have any questions about this Privacy Policy or how your data is handled, ------------------------------------------------------------------------------------------------------------ By using the Endless Host Roles mod, you agree to the terms outlined in this Privacy Policy. + + + diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index ed079dc98..505f4e111 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -1256,7 +1256,7 @@ "MenuTitle.Guessers": "★ Guesser Mode ★", "MenuTitle.CTA": "★ Custom Team Assignment ★", "MenuTitle.AFKDetection": "★ AFK Detection ★", - "ReadyCheckMessage": "The host is checking how many players are ready to start the game.\\nPlease type /ready to let the host know you're not AFK.", + "ReadyCheckMessage": "The host/an Admin is checking how many players are ready to start the game.\\nPlease type /ready to let the host know you're not AFK.", "ReadyCheckTitle": "<#ffff00>★ Ready Check ★", "EveryoneReadyTitle": "<#00ff00>Everyone is ready!", "PlayersNotReadyTitle": "<#ffff00><#ffab1b>{0} players are not ready! They are:", @@ -2625,6 +2625,7 @@ "ModeratorMuted": "Moderator {0} has muted {1} for <#00ffa5>{2} seconds.", "PlayerUnmuted": "You've successfully unmuted {0}.", "YouUnmuted": "{0} has unmuted you.", + "AdminUnmuted": "Admin {0} has unmuted {1}.", "MuteMessage": "<#ff0000>You're muted and cannot send messages. The mute will expire in <#ffff00>{0} seconds.", "WhisperTitle": "{0} <#ffffff>(ID <#00ffa5>{1}) whispers to you:", "HWhisperTitle": "<#ff0000>The Host ({0}) whispers a <#00ffff>help to you:", @@ -9851,7 +9852,7 @@ "ErrorEndTextDescription": "To avoid crashing, the game was forcibly ended.", "ForceEnd": "Aborted", "EveryoneDied": "Everyone died", - "ForceEndText": "Host has aborted the game", + "ForceEndText": "The Host/an Admin has aborted the game", "BlackScreenFixCompleteNotify": "<#00ff00>Black Screen Fix Code Executed", "MinMaxModdedImpCountsSettingsChangedAuto": "<#ffff00>Modded Min/Max Impostors Automatically Set To <#ffffff>{0}\\nBecause you only changed the vanilla setting, which gets overridden by the modded ones.", "DiscoChangeInterval": "Disco Change Color Interval", From dcacd73c036c6f6104d5f95e67eef4d24f8b0571 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Mon, 15 Sep 2025 11:29:52 +0200 Subject: [PATCH 16/24] various fixes --- CTA/CustomTeamAssigner/CustomRoles.cs | 1 + GameMode/BedWars.cs | 2 +- Main.cs | 4 ++-- Modules/CustomRolesHelper.cs | 14 ++++++++++++++ Modules/ExtendedPlayerControl.cs | 6 ++++-- Modules/GuessManager.cs | 2 +- Modules/NameNotifyManager.cs | 4 +++- Modules/Utils.cs | 6 ++++-- Patches/CheckGameEndPatch.cs | 2 +- Patches/OnGameStartedPatch.cs | 2 +- Patches/PlayerControlPatch.cs | 15 ++++++--------- Resources/Lang/en_US.json | 1 + Roles/Crewmate/Scout.cs | 12 ++---------- Roles/Impostor/Cantankerous.cs | 8 +------- Roles/Impostor/Changeling.cs | 2 +- Roles/Impostor/Eraser.cs | 2 +- Roles/Impostor/Mastermind.cs | 12 ++++-------- Roles/Impostor/Sniper.cs | 2 +- Roles/Impostor/Venerer.cs | 3 ++- Roles/Neutral/Jackal.cs | 4 +--- 20 files changed, 52 insertions(+), 52 deletions(-) diff --git a/CTA/CustomTeamAssigner/CustomRoles.cs b/CTA/CustomTeamAssigner/CustomRoles.cs index 408d0cb71..1f22880c5 100644 --- a/CTA/CustomTeamAssigner/CustomRoles.cs +++ b/CTA/CustomTeamAssigner/CustomRoles.cs @@ -289,6 +289,7 @@ public enum CustomRoles Bubble, Chemist, Cherokious, + Clerk, Collector, Curser, Deathknight, diff --git a/GameMode/BedWars.cs b/GameMode/BedWars.cs index fdb40627b..fe076d583 100644 --- a/GameMode/BedWars.cs +++ b/GameMode/BedWars.cs @@ -1753,12 +1753,12 @@ public static class BedWars } } + Breaking.Remove(pc.PlayerId); NameNotifyManager.Notifies.Remove(pc.PlayerId); Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); Layers.RemoveAt(Layers.Count - 1); data.Inventory.Adjust(topLayer, GetNextProtectReq()); UpdateStatus(); - Breaking.Remove(pc.PlayerId); if (Layers.Count == 0 && data.Team != Team) Broken(); } } diff --git a/Main.cs b/Main.cs index 453d46e9b..461818bdf 100644 --- a/Main.cs +++ b/Main.cs @@ -42,8 +42,8 @@ public class Main : BasePlugin private const string DebugKeyHash = "c0fd562955ba56af3ae20d7ec9e64c664f0facecef4b3e366e109306adeae29d"; private const string DebugKeySalt = "59687b"; private const string PluginGuid = "com.gurge44.endlesshostroles"; - public const string PluginVersion = "6.3.1"; - public const string PluginDisplayVersion = "6.3.1"; + public const string PluginVersion = "6.4.0"; + public const string PluginDisplayVersion = "6.4.0"; public const bool TestBuild = false; public const string NeutralColor = "#ffab1b"; diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 4a763db6f..7ddbd1dd6 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -1230,6 +1230,20 @@ internal static class CustomRolesHelper CustomRoles.Wyrd; } + public static bool IncompatibleWithVenom(this CustomRoles role) + { + return role switch + { + CustomRoles.Bomber when !Bomber.BomberCanKill.GetBool() => true, + CustomRoles.Changeling when !Changeling.CanKillBeforeRoleChange.GetBool() => true, + CustomRoles.Eraser when Eraser.EraseMethod.GetValue() == 0 => true, + CustomRoles.Silencer when Silencer.SilenceMode.GetValue() == 0 => true, + CustomRoles.Sniper when !Sniper.CanKillWithBullets.GetBool() => true, + CustomRoles.Augmenter or CustomRoles.BallLightning or CustomRoles.Blackmailer or CustomRoles.Cantankerous or CustomRoles.Capitalism or CustomRoles.Consort or CustomRoles.Echo or CustomRoles.EvilDiviner or CustomRoles.FireWorks or CustomRoles.Framer or CustomRoles.Gangster or CustomRoles.Generator or CustomRoles.Hangman or CustomRoles.Inhibitor or CustomRoles.Kamikaze or CustomRoles.Mastermind or CustomRoles.Morphling or CustomRoles.Ninja or CustomRoles.Nullifier or CustomRoles.Penguin or CustomRoles.Puppeteer or CustomRoles.Saboteur or CustomRoles.Sapper or CustomRoles.Scavenger or CustomRoles.Swooper or CustomRoles.Undertaker or CustomRoles.Vampire or CustomRoles.Warlock or CustomRoles.Wasp or CustomRoles.Wiper or CustomRoles.Witch or CustomRoles.YinYanger => true, + _ => false + }; + } + public static RoleTypes GetRoleTypes(this CustomRoles role) { if (Enum.TryParse(role.GetVNRole(true).ToString().Replace("EHR", ""), true, out RoleTypes type)) return type; diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 65fdcd1cd..7ea81eead 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -85,6 +85,9 @@ internal static class ExtendedPlayerControl public static bool CanUseVent(this PlayerControl player, int ventId) { + int? closestVentId = player.GetClosestVent()?.Id; + if (player.inVent && closestVentId == ventId) return true; + switch (Options.CurrentGameMode) { case CustomGameMode.RoomRush: @@ -96,9 +99,8 @@ internal static class ExtendedPlayerControl } if (player.Is(CustomRoles.Trainee) && MeetingStates.FirstMeeting) return false; - if (player.Is(CustomRoles.Blocked) && player.GetClosestVent()?.Id != ventId) return false; + if (player.Is(CustomRoles.Blocked) && closestVentId != ventId) return false; if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || Main.Invisible.Contains(player.PlayerId)) return false; - if (player.inVent && player.GetClosestVent()?.Id == ventId) return true; return (player.CanUseImpostorVentButton() || player.GetRoleTypes() == RoleTypes.Engineer) && Main.PlayerStates.Values.All(x => x.Role.CanUseVent(player, ventId)); } diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs index 33d4c5924..aa9144a5e 100644 --- a/Modules/GuessManager.cs +++ b/Modules/GuessManager.cs @@ -142,7 +142,7 @@ public static class GuessManager if ((pc.IsCrewmate() && target.IsCrewmate() && !Options.CrewCanGuessCrew.GetBool()) || (pc.IsImpostor() && target.IsImpostor() && !Options.ImpCanGuessImp.GetBool())) { - ShowMessage("GuessNotAllowed"); + ShowMessage("GuessTeamMate"); return true; } diff --git a/Modules/NameNotifyManager.cs b/Modules/NameNotifyManager.cs index 3c9697a45..96c0b7db8 100644 --- a/Modules/NameNotifyManager.cs +++ b/Modules/NameNotifyManager.cs @@ -20,7 +20,9 @@ public static class NameNotifyManager { if (!AmongUsClient.Instance.AmHost || pc == null) return; if (!GameStates.IsInTask) return; - if (!text.Contains("")) text = Utils.ColorString(Color.white, text); + + text = text.Trim(); + if (!text.Contains("") && !text.Contains("<#")) text = Utils.ColorString(Color.white, text); if (!text.Contains("{text}"; long expireTS = Utils.TimeStamp + (long)time; diff --git a/Modules/Utils.cs b/Modules/Utils.cs index e8dc17197..9a15455eb 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -298,8 +298,10 @@ public static class Utils { if (mapId is 1 or 5) { - var hqHudSystemType = systemType.CastFast(); - return hqHudSystemType is { IsActive: true }; + var hqHudSystemType = systemType.TryCast(); + // ReSharper disable once MergeIntoPattern + // For some reason, the game sometimes crashes here when trying to get_IsActive + return hqHudSystemType != null && hqHudSystemType.IsActive; } var hudOverrideSystemType = systemType.CastFast(); diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index 5f9b52913..c555de4a2 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -593,7 +593,7 @@ internal static class GameEndChecker foreach (PlayerState playerState in statesCoutingAsCrew) { - if (playerState.Player == null || !playerState.Player.IsAlive()) continue; + if (playerState.Player == null || !playerState.Player.IsAlive() || !Options.CrewAdvancedGameEndCheckingSettings[playerState.MainRole].GetBool()) continue; playerState.Role.ManipulateGameEndCheckCrew(out bool keepGameGoing, out int countsAs); crewKeepsGameGoing |= keepGameGoing; crew += countsAs - 1; diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index 3114787d0..4eb425fb4 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -739,7 +739,7 @@ internal static class StartGameHostPatch } } - if (kp.Value.IsImpostor() && kp.Value.GetRoleTypes() == RoleTypes.Impostor && !venomBanned) + if (kp.Value.IsImpostor() && kp.Value.GetRoleTypes() == RoleTypes.Impostor && !kp.Value.IncompatibleWithVenom() && !venomBanned) venomList.Add(player.PlayerId); } } diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index ff45d2e86..de5af2cfb 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -2012,7 +2012,7 @@ internal static class FixedUpdatePatch { target.cosmetics.nameText.text = changeTo; - var offset = 0.2f; + var offset = 0.1f; if (self && NameNotifyManager.GetNameNotify(seer, out string notify) && notify.Contains('\n')) { @@ -2039,7 +2039,7 @@ internal static class FixedUpdatePatch offset += 0.8f; if (Options.LargerRoleTextSize.GetBool()) - offset += 0.15f; + offset += 0.05f; roleText.transform.SetLocalY(offset); target.cosmetics.colorBlindText.transform.SetLocalY(-(offset + 0.2f)); @@ -2048,7 +2048,7 @@ internal static class FixedUpdatePatch else { // Restoring the position text coordinates to their initial values - roleText.transform.SetLocalY(0.2f); + roleText.transform.SetLocalY(0.1f); } } @@ -2110,9 +2110,7 @@ internal static class PlayerStartPatch if (__result || __instance == null || __instance.__4__this == null || __instance.__4__this.PlayerId >= 254 || __instance.__4__this.cosmetics == null) return; TextMeshPro nameText = __instance.__4__this.cosmetics.nameText; TextMeshPro roleText = Object.Instantiate(nameText, nameText.transform, true); - bool largerFontSize = Options.LargerRoleTextSize.GetBool(); - roleText.transform.localPosition = new(0f, largerFontSize ? 0.35f : 0.2f, 0f); - if (!largerFontSize) roleText.fontSize -= 0.9f; + if (!Options.LargerRoleTextSize.GetBool()) roleText.fontSize -= 0.9f; roleText.text = "RoleText"; roleText.gameObject.name = "RoleText"; roleText.enabled = false; @@ -2275,7 +2273,7 @@ internal static class GameDataCompleteTaskPatch { public static void Postfix(PlayerControl pc, uint taskId) { - if (GameStates.IsMeeting) return; + if (MeetingHud.Instance && MeetingHud.Instance.state != MeetingHud.VoteStates.Animating) return; if (Options.CurrentGameMode == CustomGameMode.HideAndSeek && CustomHnS.PlayerRoles[pc.PlayerId].Interface.Team == Team.Crewmate && pc.IsAlive()) { @@ -2294,8 +2292,7 @@ internal static class PlayerControlCompleteTaskPatch { public static bool Prefix(PlayerControl __instance) { - if (GameStates.IsMeeting) return false; - + if (MeetingHud.Instance && MeetingHud.Instance.state != MeetingHud.VoteStates.Animating) return false; return !Workhorse.OnCompleteTask(__instance) && Capitalism.AddTaskForPlayer(__instance); // Cancel task win } diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index ed079dc98..4c4cc085b 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -2263,6 +2263,7 @@ "BittenByInfectious": "You were infected by the Infectious!", "InfectiousBittenPlayer": "You successfully infected a player", "GuessNotAllowed": "Sorry, your role does not have access to guessing.", + "GuessTeamMate": "Sorry, you can't guess your teammates.", "GuessOnbound": "This player has the Onbound add-on, so your guess on them was canceled.", "GuessPhantom": "You can't guess a Specter, that allows them to win!", "DovesOfNeaceOnGuard": "Ability used, {0} uses remain", diff --git a/Roles/Crewmate/Scout.cs b/Roles/Crewmate/Scout.cs index d311922dd..72e82f8bc 100644 --- a/Roles/Crewmate/Scout.cs +++ b/Roles/Crewmate/Scout.cs @@ -29,8 +29,6 @@ public class Scout : RoleBase public override bool IsEnable => PlayerIdList.Count > 0; - public override bool SeesArrowsToDeadBodies => true; - public override void SetupCustomOption() { SetupRoleOptions(Id, TabGroup.CrewmateRoles, CustomRoles.Scout); @@ -127,14 +125,7 @@ public class Scout : RoleBase public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) { - if (seer == null || seer.PlayerId != TrackerId) return string.Empty; - - if (target != null && seer.PlayerId != target.PlayerId) return string.Empty; - - if (!TrackerTarget.ContainsKey(seer.PlayerId)) return string.Empty; - - if (GameStates.IsMeeting) return string.Empty; - + if (seer == null || seer.PlayerId != TrackerId || target != null && seer.PlayerId != target.PlayerId || !TrackerTarget.ContainsKey(seer.PlayerId) || GameStates.IsMeeting) return string.Empty; return TrackerTarget[seer.PlayerId].Aggregate(string.Empty, (current, trackTarget) => current + Utils.ColorString(CanGetColoredArrow.GetBool() ? Main.PlayerColors[trackTarget] : Color.white, TargetArrow.GetArrows(seer, trackTarget))) + LocateArrow.GetArrows(seer); } @@ -173,6 +164,7 @@ public class Scout : RoleBase { kvp.Value.Remove(player.PlayerId); TargetArrow.Remove(kvp.Key, player.PlayerId); + LocateArrow.Add(kvp.Key, player.Pos()); } } } diff --git a/Roles/Impostor/Cantankerous.cs b/Roles/Impostor/Cantankerous.cs index 06421db43..aa31f50af 100644 --- a/Roles/Impostor/Cantankerous.cs +++ b/Roles/Impostor/Cantankerous.cs @@ -68,14 +68,8 @@ public class Cantankerous : RoleBase } } - public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) + public override void OnMurder(PlayerControl killer, PlayerControl target) { - if (!base.OnCheckMurder(killer, target)) return false; - - if (killer.GetAbilityUseLimit() <= 0) return false; - killer.RpcRemoveAbilityUse(); - - return true; } } \ No newline at end of file diff --git a/Roles/Impostor/Changeling.cs b/Roles/Impostor/Changeling.cs index f792f7160..f5bff642c 100644 --- a/Roles/Impostor/Changeling.cs +++ b/Roles/Impostor/Changeling.cs @@ -12,7 +12,7 @@ internal class Changeling : RoleBase public static readonly Dictionary ChangedRole = []; private static OptionItem CanPickPartnerRole; - private static OptionItem CanKillBeforeRoleChange; + public static OptionItem CanKillBeforeRoleChange; private static OptionItem AvailableRoles; private static readonly string[] AvailableRolesMode = diff --git a/Roles/Impostor/Eraser.cs b/Roles/Impostor/Eraser.cs index 28a6a1353..c53b80d54 100644 --- a/Roles/Impostor/Eraser.cs +++ b/Roles/Impostor/Eraser.cs @@ -23,7 +23,7 @@ internal class Eraser : RoleBase ]; private static OptionItem EraseLimitOpt; - private static OptionItem EraseMethod; + public static OptionItem EraseMethod; private static OptionItem WhenTargetIsNeutral; public static OptionItem HideVote; public static OptionItem CancelVote; diff --git a/Roles/Impostor/Mastermind.cs b/Roles/Impostor/Mastermind.cs index 4436a1b72..0a45bcbf6 100644 --- a/Roles/Impostor/Mastermind.cs +++ b/Roles/Impostor/Mastermind.cs @@ -106,14 +106,10 @@ public class Mastermind : RoleBase ManipulateDelays.Remove(x.Key); ManipulatedPlayers.TryAdd(x.Key, TimeStamp); - if (pc.HasKillButton()) - { - TempKCDs.TryAdd(pc.PlayerId, Main.KillTimers[pc.PlayerId]); - pc.SetKillCooldown(1f); - } - else - pc.RpcChangeRoleBasis(CustomRoles.SerialKiller); - + if (pc.HasKillButton()) TempKCDs.TryAdd(pc.PlayerId, Main.KillTimers[pc.PlayerId]); + else pc.RpcChangeRoleBasis(CustomRoles.SerialKiller); + + pc.SetKillCooldown(1f); NotifyRoles(SpecifySeer: MastermindPC, SpecifyTarget: pc); } } diff --git a/Roles/Impostor/Sniper.cs b/Roles/Impostor/Sniper.cs index 4af5fb4cc..8c6b25942 100644 --- a/Roles/Impostor/Sniper.cs +++ b/Roles/Impostor/Sniper.cs @@ -20,7 +20,7 @@ public class Sniper : RoleBase private static OptionItem SniperAimAssist; private static OptionItem SniperAimAssistOnshot; private static OptionItem ShapeshiftDuration; - private static OptionItem CanKillWithBullets; + public static OptionItem CanKillWithBullets; private static bool MeetingReset; private static int MaxBulletCount; diff --git a/Roles/Impostor/Venerer.cs b/Roles/Impostor/Venerer.cs index 647bf63c3..38feb61c5 100644 --- a/Roles/Impostor/Venerer.cs +++ b/Roles/Impostor/Venerer.cs @@ -88,12 +88,13 @@ public class Venerer : RoleBase switch (Stage) { case 1: + var outfit = pc.Data.DefaultOutfit; Utils.RpcChangeSkin(pc, new NetworkedPlayerInfo.PlayerOutfit().Set("", 15, "", "", "", "", "")); ChangedSkin = true; LateTask.New(() => { if (!ChangedSkin || pc == null || !pc.IsAlive()) return; - Utils.RpcChangeSkin(pc, Camouflage.PlayerSkins[pc.PlayerId]); + Utils.RpcChangeSkin(pc, outfit); ChangedSkin = false; }, AbilityDuration.GetInt(), log: false); break; diff --git a/Roles/Neutral/Jackal.cs b/Roles/Neutral/Jackal.cs index 024db953a..750509a95 100644 --- a/Roles/Neutral/Jackal.cs +++ b/Roles/Neutral/Jackal.cs @@ -193,10 +193,8 @@ public class Jackal : RoleBase private static bool CanBeSidekick(PlayerControl pc) { if (!CanRecruitImpostors.GetBool() && pc.Is(CustomRoleTypes.Impostor)) return false; - if (!CanRecruitMadmates.GetBool() && pc.IsMadmate()) return false; - - return pc != null && !pc.Is(CustomRoles.Sidekick) && !pc.Is(CustomRoles.Curser) && !pc.Is(CustomRoles.Loyal) && !pc.IsConverted() && pc.GetCustomRole().IsAbleToBeSidekicked(); + return pc != null && !pc.Is(CustomRoles.Sidekick) && !pc.Is(CustomRoles.Curser) && !pc.Is(CustomRoles.Loyal) && !pc.Is(CustomRoles.Bloodlust) && !pc.IsConverted() && pc.GetCustomRole().IsAbleToBeSidekicked(); } public override void OnFixedUpdate(PlayerControl pc) From fc57ca87a019cc2a45b6406a86ce7438ebc188b4 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Mon, 15 Sep 2025 15:11:00 +0200 Subject: [PATCH 17/24] Add Compelled, Inquisitor, Postponer, Tree --- CustomRoles.cs | 4 ++ Main.cs | 5 ++ Modules/CustomNetObject.cs | 9 +++ Modules/CustomRolesHelper.cs | 10 +++- Modules/FixedUpdateCaller.cs | 12 ++++ Modules/OptionHolder.cs | 11 ++++ Modules/Utils.cs | 8 +++ Patches/ChatCommandPatch.cs | 13 ++++- Patches/IntroPatch.cs | 2 + Patches/MeetingHudPatch.cs | 3 + Patches/OutroPatch.cs | 1 + Patches/PlayerControlPatch.cs | 8 +++ Patches/PlayerJoinAndLeftPatch.cs | 1 + Resources/Lang/en_US.json | 18 ++++++ Roles/AddOns/Compelled.cs | 11 ++++ Roles/Crewmate/Inquisitor.cs | 68 ++++++++++++++++++++++ Roles/Crewmate/Tree.cs | 95 +++++++++++++++++++++++++++++++ Roles/Impostor/AntiAdminer.cs | 2 +- Roles/Impostor/Postponer.cs | 63 ++++++++++++++++++++ 19 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 Roles/AddOns/Compelled.cs create mode 100644 Roles/Crewmate/Inquisitor.cs create mode 100644 Roles/Crewmate/Tree.cs create mode 100644 Roles/Impostor/Postponer.cs diff --git a/CustomRoles.cs b/CustomRoles.cs index 1f22880c5..e3d38adbc 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -86,6 +86,7 @@ public enum CustomRoles Overheat, Parasite, Penguin, + Postponer, Psychopath, Puppeteer, QuickShooter, @@ -201,6 +202,7 @@ public enum CustomRoles Insight, ParityCop, // Inspector Inquirer, + Inquisitor, Jailor, Journalist, Judge, @@ -266,6 +268,7 @@ public enum CustomRoles Transmitter, Transporter, Tracefinder, + Tree, Tunneler, Ventguard, Veteran, @@ -489,6 +492,7 @@ public enum CustomRoles Cleansed, Clumsy, Commited, + Compelled, Contagious, Damocles, Deadlined, diff --git a/Main.cs b/Main.cs index 461818bdf..d4e7c9405 100644 --- a/Main.cs +++ b/Main.cs @@ -154,6 +154,8 @@ public class Main : BasePlugin public static Dictionary NumEmergencyMeetingsUsed = []; public static int MadmateNum; public static uint LobbyBehaviourNetId; + + public static float GameTimer; public static bool ShowResult = true; @@ -415,6 +417,8 @@ public class Main : BasePlugin { CustomRoles.Gardener, "#00ff00" }, { CustomRoles.Farmer, "#FFDE59" }, { CustomRoles.Transmitter, "#c9a11e" }, + { CustomRoles.Tree, "#00ff00" }, + { CustomRoles.Inquisitor, "#7726B6" }, { CustomRoles.Imitator, "#c99e28" }, { CustomRoles.PortalMaker, "#700078" }, { CustomRoles.Ankylosaurus, "#7FE44C" }, @@ -642,6 +646,7 @@ public class Main : BasePlugin { CustomRoles.Listener, "#060270" }, { CustomRoles.Unbound, "#DFC57B" }, { CustomRoles.AntiTP, "#fcba03" }, + { CustomRoles.Compelled, "#D2E44C" }, { CustomRoles.Commited, "#f5c542" }, { CustomRoles.BananaMan, "#ffe135" }, { CustomRoles.Blind, "#666666" }, diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs index d80523140..cee4d87f6 100644 --- a/Modules/CustomNetObject.cs +++ b/Modules/CustomNetObject.cs @@ -12,6 +12,7 @@ using InnerNet; using TMPro; using UnityEngine; using GameStates = EHR.GameStates; +using Tree = EHR.Crewmate.Tree; // Credit: https://github.com/Rabek009/MoreGamemodes/blob/e054eb498094dfca0a365fc6b6fea8d17f9974d7/Modules/AllObjects // Huge thanks to Rabek009 for this code! @@ -681,6 +682,14 @@ namespace EHR } } + internal sealed class FallenTree : CustomNetObject + { + public FallenTree(Vector2 position) + { + CreateNetObject(Tree.FallenSprite, position); + } + } + internal sealed class ShapeshiftMenuElement : CustomNetObject { private readonly byte VisibleTo; diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 7ddbd1dd6..3449f987d 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -40,6 +40,7 @@ internal static class CustomRolesHelper CustomRoles.Gardener, CustomRoles.Farmer, CustomRoles.Explosivist, + CustomRoles.Tree, // Add-ons CustomRoles.Energetic, @@ -151,6 +152,7 @@ internal static class CustomRolesHelper CustomRoles.Forger => CustomRoles.Impostor, CustomRoles.ClockBlocker => CustomRoles.Impostor, CustomRoles.Psychopath => CustomRoles.Impostor, + CustomRoles.Postponer => CustomRoles.Impostor, CustomRoles.Venerer => CustomRoles.Shapeshifter, CustomRoles.Kidnapper => CustomRoles.Shapeshifter, CustomRoles.Stasis => UsePets ? CustomRoles.Impostor : CustomRoles.Shapeshifter, @@ -233,6 +235,8 @@ internal static class CustomRolesHelper CustomRoles.Bane => CustomRoles.Crewmate, CustomRoles.Farmer => CustomRoles.Crewmate, CustomRoles.Transmitter => CustomRoles.Crewmate, + CustomRoles.Tree => CustomRoles.Crewmate, + CustomRoles.Inquisitor => CustomRoles.Crewmate, CustomRoles.Gardener => CustomRoles.Crewmate, CustomRoles.Imitator => CustomRoles.Crewmate, CustomRoles.PortalMaker => CustomRoles.Crewmate, @@ -710,6 +714,7 @@ internal static class CustomRolesHelper CustomRoles.Wiper or CustomRoles.Psychopath or CustomRoles.Kidnapper or + CustomRoles.Postponer or CustomRoles.Venerer or CustomRoles.ClockBlocker or CustomRoles.Forger or @@ -1628,6 +1633,7 @@ internal static class CustomRolesHelper CustomRoles.Miner => RoleOptionType.Impostor_Concealing, CustomRoles.Morphling => RoleOptionType.Impostor_Concealing, CustomRoles.Penguin => RoleOptionType.Impostor_Concealing, + CustomRoles.Postponer => RoleOptionType.Impostor_Concealing, CustomRoles.Puppeteer => RoleOptionType.Impostor_Concealing, CustomRoles.RiftMaker => RoleOptionType.Impostor_Concealing, CustomRoles.Scavenger => RoleOptionType.Impostor_Concealing, @@ -1712,9 +1718,10 @@ internal static class CustomRolesHelper CustomRoles.MeetingManager => RoleOptionType.Crewmate_Investigate, CustomRoles.Ignitor => RoleOptionType.Crewmate_Investigate, CustomRoles.Imitator => RoleOptionType.Crewmate_Investigate, + CustomRoles.Inquirer => RoleOptionType.Crewmate_Investigate, + CustomRoles.Inquisitor => RoleOptionType.Crewmate_Investigate, CustomRoles.Insight => RoleOptionType.Crewmate_Investigate, CustomRoles.ParityCop => RoleOptionType.Crewmate_Investigate, - CustomRoles.Inquirer => RoleOptionType.Crewmate_Investigate, CustomRoles.Leery => RoleOptionType.Crewmate_Investigate, CustomRoles.Lighter => RoleOptionType.Crewmate_Investigate, CustomRoles.Lookout => RoleOptionType.Crewmate_Investigate, @@ -1809,6 +1816,7 @@ internal static class CustomRolesHelper CustomRoles.Randomizer => RoleOptionType.Crewmate_Chaos, CustomRoles.ToiletMaster => RoleOptionType.Crewmate_Chaos, CustomRoles.Tornado => RoleOptionType.Crewmate_Chaos, + CustomRoles.Tree => RoleOptionType.Crewmate_Chaos, _ => role.IsImpostor() ? RoleOptionType.Impostor_Miscellaneous : RoleOptionType.Neutral_Benign }; } diff --git a/Modules/FixedUpdateCaller.cs b/Modules/FixedUpdateCaller.cs index 4bf8c6e4a..5104b439c 100644 --- a/Modules/FixedUpdateCaller.cs +++ b/Modules/FixedUpdateCaller.cs @@ -201,6 +201,18 @@ public static class FixedUpdateCaller } } catch (Exception e) { Utils.ThrowException(e); } + + try + { + Main.GameTimer += Time.fixedDeltaTime; + + if (Options.EnableGameTimeLimit.GetBool() && Main.GameTimer > Options.GameTimeLimit.GetInt()) + { + Main.GameTimer = 0f; + CustomWinnerHolder.ResetAndSetWinner(CustomWinner.None); + } + } + catch (Exception e) { Utils.ThrowException(e); } } } catch (Exception e) { Utils.ThrowException(e); } diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index dc8f17863..88a75679b 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -736,6 +736,8 @@ public static class Options public static OptionItem ShowTaskCountWhenAlive; public static OptionItem ShowTaskCountWhenDead; public static OptionItem IntegrateNaturalDisasters; + public static OptionItem EnableGameTimeLimit; + public static OptionItem GameTimeLimit; public static OptionItem ShowDifferentEjectionMessageForSomeRoles; public static OptionItem ShowAntiBlackoutWarning; public static OptionItem AllowConsole; @@ -2942,6 +2944,15 @@ public static class Options .SetColor(new Color32(193, 255, 209, byte.MaxValue)) .RegisterUpdateValueEvent((_, _, _) => GameOptionsMenuPatch.ReloadUI()); + EnableGameTimeLimit = new BooleanOptionItem(24455, "EnableGameTimeLimit", false, TabGroup.GameSettings) + .SetColor(new Color32(193, 255, 209, byte.MaxValue)) + .SetHeader(true); + + GameTimeLimit = new FloatOptionItem(24456, "GameTimeLimit", new(20f, 3600f, 20f), 900f, TabGroup.GameSettings) + .SetColor(new Color32(193, 255, 209, byte.MaxValue)) + .SetParent(EnableGameTimeLimit) + .SetValueFormat(OptionFormat.Seconds); + new TextOptionItem(100029, "MenuTitle.Ghost", TabGroup.GameSettings) .SetGameMode(CustomGameMode.Standard) diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 9a15455eb..93e10c51b 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -30,6 +30,7 @@ using InnerNet; using Newtonsoft.Json; using UnityEngine; using static EHR.Translator; +using Tree = EHR.Crewmate.Tree; namespace EHR; /* @@ -2436,6 +2437,12 @@ public static class Utils sender.RpcSetName(seer, Car.Name); return true; } + + if (Main.PlayerStates.TryGetValue(seer.PlayerId, out var seerState) && seerState.Role is Tree { TreeSpriteActive: true }) + { + sender.RpcSetName(seer, Tree.Sprite); + return true; + } if (forMeeting && Magistrate.CallCourtNextMeeting) { @@ -3230,6 +3237,7 @@ public static class Utils CustomRoles.Gardener => Gardener.AbilityCooldown.GetInt(), CustomRoles.Mayor when Mayor.MayorHasPortableButton.GetBool() => (int)Math.Round(Options.AdjustedDefaultKillCooldown), CustomRoles.Paranoia => (int)Math.Round(Options.AdjustedDefaultKillCooldown), + CustomRoles.Tree => 5 + (includeDuration ? Tree.FallDelay.GetInt() + Tree.FallStunDuration.GetInt() : 0), CustomRoles.Grenadier => Options.GrenadierSkillCooldown.GetInt() + (includeDuration ? Options.GrenadierSkillDuration.GetInt() : 0), CustomRoles.Lighter => Options.LighterSkillCooldown.GetInt() + (includeDuration ? Options.LighterSkillDuration.GetInt() : 0), CustomRoles.SecurityGuard => Options.SecurityGuardSkillCooldown.GetInt() + (includeDuration ? Options.SecurityGuardSkillDuration.GetInt() : 0), diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 069b23b9f..0a5065345 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -219,7 +219,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(["revive", "воскрешение", "воскрешать", "复活", "reviver"], "{id}", GetString("CommandDescription.Revive"), Command.UsageLevels.Host, Command.UsageTimes.InGame, ReviveCommand, true, false, [GetString("CommandArgs.Revive.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 @@ -482,6 +483,16 @@ internal static class ChatCommands // --------------------------------------------------------------------------------------------------------------------------------------------- + private static void ReviveCommand(PlayerControl player, string text, string[] args) + { + if ((!Options.NoGameEnd.GetBool() && !player.FriendCode.GetDevUser().up) || args.Length < 2 || !byte.TryParse(args[1], out byte targetId)) return; + + PlayerControl target = Utils.GetPlayerById(targetId); + if (target == null) return; + + target.RpcRevive(); + } + private static void ConfirmAuthCommand(PlayerControl player, string text, string[] args) { if (GameStates.CurrentServerType != GameStates.ServerType.Vanilla) diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index 74d5c8dd8..da444fca6 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -1242,6 +1242,8 @@ internal static class IntroCutsceneDestroyPatch LateTask.New(() => { + Main.GameTimer = 0f; + if (SubmergedCompatibility.IsSubmerged()) { foreach (PlayerControl pc in Main.AllAlivePlayerControls) diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index a4d49ff38..4a8bc4887 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -158,6 +158,9 @@ internal static class CheckForEndVotingPatch voteLog.Info($"{voter.GetNameWithRole().RemoveHtmlTags()} Skip for not voting"); break; } + + if (voter.Is(CustomRoles.Compelled)) + TryAddAfterMeetingDeathPlayers(PlayerState.DeathReason.Suicide, ps.TargetPlayerId); } } diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index 2dc1d1c0a..a0ac6a013 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -118,6 +118,7 @@ internal static class EndGamePatch ChatCommands.Spectators.Clear(); Utils.NumSnapToCallsThisRound = 0; + Main.GameTimer = 0f; switch (Options.CurrentGameMode) { diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index de5af2cfb..fd9a99d99 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -22,6 +22,7 @@ using TMPro; using UnityEngine; using static EHR.Translator; using static EHR.Utils; +using Tree = EHR.Crewmate.Tree; namespace EHR; @@ -1784,6 +1785,13 @@ internal static class FixedUpdatePatch return; } + if (Main.PlayerStates.TryGetValue(target.PlayerId, out var targetState) && targetState.Role is Tree { TreeSpriteActive: true }) + { + target.cosmetics.nameText.text = Tree.Sprite; + roleText.enabled = false; + return; + } + Mark.Clear(); Suffix.Clear(); diff --git a/Patches/PlayerJoinAndLeftPatch.cs b/Patches/PlayerJoinAndLeftPatch.cs index ee3c73f75..208b7ef5b 100644 --- a/Patches/PlayerJoinAndLeftPatch.cs +++ b/Patches/PlayerJoinAndLeftPatch.cs @@ -41,6 +41,7 @@ internal static class OnGameJoinedPatch GameStates.InGame = false; ErrorText.Instance?.Clear(); ChatCommands.VotedToStart = []; + Main.GameTimer = 0f; Utils.DirtyName = []; diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 05784cfc6..9183f07f3 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -2018,6 +2018,8 @@ "RomanticArrows": "Romantic has arrows pointing to their Partner", "RomanticPartnerHasArrows": "Partner has arrows pointing to the Romantic", "SpiritualistTargetMessage": "The Spiritualist has an arrow pointing toward you", + "EnableGameTimeLimit": "Enable Game Time Limit (Expired = No One Wins)", + "GameTimeLimit": "Game Time Limit", "Gaslighter": "Gaslighter", "GaslighterInfo": "Make others doubt their own memory", "GaslighterInfoLong": "(Neutrals):\\nAs the Gaslighter, you do different actions through your kill button every round.\\n\\nRound 1: Normal kills\\nRound 2: Knight players\\nRound 3: Curse players (like Witch)\\nRound 4: Shield players\\n\\nThen the cycle repeats, or you stay on normally killing until the end of the game depending on the settings.\\nSettings determine your win condition.", @@ -2487,6 +2489,7 @@ "CommandDescription.Imitate": "Imitate a dead player's role as the Imitator", "CommandDescription.Retribute": "Guess who killed the player you camped as the Retributionist", "CommandDescription.ConfirmAuth": "Command required to register a new EHR account", + "CommandDescription.Revive": "Revives a dead player. Requires special access or No Game End enabled.", "CommandArgs.ChangeSetting.Name": "The setting's name to change (leave blank to view all settings)", "CommandArgs.ChangeSetting.UnknownValue": "(leave blank to view what to put here)", "CommandArgs.Rename.Name": "Your new name", @@ -2579,6 +2582,7 @@ "CommandArgs.Imitate.Id": "The dead player's ID to imitate the role of", "CommandArgs.Retribute.Id": "The potential killer's ID to guess", "CommandArgs.ConfirmAuth.UUID": "The unique token provided by the server", + "CommandArgs.Revive.Id": "The dead player's ID to revive", "Commands.ChangeSettingHelp": "Arguments:\\n- map (theskeld/mirahq/polus/dlekseht/airship/thefungle/custom)\\n- impostors (number)\\n- players (number)\\n- recommended (on/off)\\n- confirmejects (on/off)\\n- emergencymeetings (number)\\n- anonymousvotes (on/off)\\n- emergencycooldown (number)\\n- discussiontime (number)\\n- votingtime (number)\\n- playerspeed (float)\\n- crewmatevision (float)\\n- impostorvision (float)\\n- killcooldown (float)\\n- killdistance (short/medium/long/custom)\\n- taskbarupdates (always/meetings/never)\\n- visualtasks (on/off)\\n- commontasks (number)\\n- longtasks (number)\\n- shorttasks (number)\\n- scientistcount (number)\\n- scientistchance (number)\\n- vitalsdisplaycooldown (float)\\n- batteryduration (float)\\n- engineercount (number)\\n- engineerchance (number)\\n- ventusecooldown (float)\\n- maxtimeinvents (float)\\n- guardianangelcount (number)\\n- guardianangelchance (number)\\n- protectcooldown (float)\\n- protectduration (float)\\n- protectvisibletoimpostors (on/off)\\n- shapeshiftercount (number)\\n- shapeshifterchance (number)\\n- shapeshiftduration (float)\\n- shapeshiftcooldown (float)\\n- leaveshapeshiftevidence (on/off)\\n- phantomcount (number)\\n- phantomchance (number)\\n- inviscooldown (float)\\n- invisduration (float)\\n- noisemakercount (number)\\n- noisemakerchance (number)\\n- noisemakerimpostoralert (on/off)\\n- noisemakeralertduration (number)\\n- trackercount (number)\\n- trackerchance (number)\\n- trackduration (float)\\n- trackcooldown (float)\\n- trackdelay (float)\\n- ghostdotasks (on/off)\\n\\nNotes:\\n1. Upper and lowercase matters\\n2. Where it says custom, you obviously put a number argument after it\\n3. A float is a single precision floating number, like 1.7\\n4. \"ghostdotasks\" causes issues if you set it\\n5. Example uses:\\n - \"/cs map polus\"\\n - \"/cs killdistance custom 4\"\\n - \"/cs trackdelay 1,7\"\\n - \"/cs confirmejects on\"", "AddTagSuccess": "<#00ffa5>✓ Suceessfully created the tag {0} for {1}. If you want to change it, just use this command again and it will override the current tag. To delete it, simply type <#00a5ff>/deletetag {2}!", "DeleteTagSuccess": "<#00ffa5>✓ Successfully deleted {0}'s tag.", @@ -8981,6 +8985,20 @@ "VenomInfo": "Kill to Dissolve Bodies", "VenomInfoLong": "(Impostors):\\nGrants Viper ability for impostor roles.", "Prefix.Venom": "Venom", + "Inquisitor": "Inquisitor", + "InquisitorInfo": "Check Who Knows Whose Role", + "InquisitorInfoLong": "(Crewmates):\\nAs the Inquisitor, you may vote someone in a meeting to see whose role they know.", + "Inquisitor.KnowExactRolesAfterTasksFinished": "Know Exact Roles After Finishing All Tasks", + "Inquisitor.ExcludeDeadPlayers": "Exclude Dead Players' Roles Seen", + "InquisitorVoteResult": "{0} knows the role of:\\n{1}", + "InquisitorNoInfo": "No one, other than themselves", + "Compelled": "Compelled", + "CompelledInfo": "Must Vote Or Suicide", + "CompelledInfoLong": "(Add-Ons):\\nYou must vote in every meeting, or you suicide.", + "Prefix.Compelled": "Compelled", + "Postponer": "Postponer", + "PostponerInfo": "Postpone Dead Body Spawning", + "PostponerInfoLong": "(Impostors):\\nAs the Postponer, when you kill, no dead body spawns instantly. Instead, it will spawn after a delay (in the same place as the murder).", "Clairvoyant": "Clairvoyant", "ClairvoyantInfo": "Know Remaining Killer Counts", "ClairvoyantInfoLong": "(Crewmates):\\nAs the Clairvoyant, you can see the remaining impostor and neutral killer counts by entering a vent or petting.", diff --git a/Roles/AddOns/Compelled.cs b/Roles/AddOns/Compelled.cs new file mode 100644 index 000000000..d6ea1089b --- /dev/null +++ b/Roles/AddOns/Compelled.cs @@ -0,0 +1,11 @@ +namespace EHR.AddOns; + +public class Compelled : IAddon +{ + public AddonTypes Type => AddonTypes.Harmful; + + public void SetupCustomOption() + { + Options.SetupAdtRoleOptions(654980, CustomRoles.Compelled, canSetNum: true, teamSpawnOptions: true); + } +} \ No newline at end of file diff --git a/Roles/Crewmate/Inquisitor.cs b/Roles/Crewmate/Inquisitor.cs new file mode 100644 index 000000000..b37ce6523 --- /dev/null +++ b/Roles/Crewmate/Inquisitor.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using EHR.Neutral; + +namespace EHR.Crewmate; + +public class Inquisitor : RoleBase +{ + public static bool On; + + public override bool IsEnable => On; + + private static OptionItem KnowExactRolesAfterTasksFinished; + private static OptionItem ExcludeDeadPlayers; + private static OptionItem AbilityUseLimit; + private static OptionItem AbilityUseGainWithEachTaskCompleted; + private static OptionItem AbilityChargesWhenFinishedTasks; + + public override void SetupCustomOption() + { + StartSetup(654900) + .AutoSetupOption(ref KnowExactRolesAfterTasksFinished, true) + .AutoSetupOption(ref ExcludeDeadPlayers, false) + .AutoSetupOption(ref AbilityUseLimit, 1f, new FloatValueRule(0, 20, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityUseGainWithEachTaskCompleted, 0.5f, new FloatValueRule(0f, 5f, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityChargesWhenFinishedTasks, 0.2f, new FloatValueRule(0f, 5f, 0.05f), OptionFormat.Times); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + playerId.SetAbilityUseLimit(AbilityUseLimit.GetFloat()); + } + + 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; + + PlayerControl[] players = ExcludeDeadPlayers.GetBool() ? Main.AllAlivePlayerControls : Main.AllPlayerControls; + List<(byte Id, CustomRoles Role)> knownRoles = Main.PlayerStates.Values.SelectMany(s => from pc in players where s.Role.KnowRole(target, pc) select (pc.PlayerId, pc.GetCustomRole())).DistinctBy(x => x.PlayerId).ToList(); + + string result; + + if (knownRoles.Count == 0) + result = Translator.GetString("InquisitorNoInfo"); + else if (KnowExactRolesAfterTasksFinished.GetBool() && voter.GetTaskState().IsTaskFinished) + result = string.Join('\n', knownRoles.Select(x => $"{x.Id.ColoredPlayerName()}: {x.Role.ToColoredString()}")); + else + result = string.Join(", ", knownRoles.Select(x => x.Id.ColoredPlayerName())); + + Utils.SendMessage("\n", voter.PlayerId, string.Format(Translator.GetString("InquisitorVoteResult"), target.PlayerId.ColoredPlayerName(), result)); + + voter.RpcRemoveAbilityUse(); + Main.DontCancelVoteList.Add(voter.PlayerId); + return true; + } + + public override void OnMeetingShapeshift(PlayerControl shapeshifter, PlayerControl target) + { + OnVote(shapeshifter, target); + } +} \ No newline at end of file diff --git a/Roles/Crewmate/Tree.cs b/Roles/Crewmate/Tree.cs new file mode 100644 index 000000000..281fe69aa --- /dev/null +++ b/Roles/Crewmate/Tree.cs @@ -0,0 +1,95 @@ +using AmongUs.GameOptions; + +namespace EHR.Crewmate; + +public class Tree : RoleBase +{ + public static bool On; + + public const string Sprite = "██<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>███
█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>██
<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
███<#711e1e>█<#711e1e>████
███<#711e1e>█<#711e1e>████
███<#711e1e>█<#711e1e>████
"; + public const string FallenSprite = "████████
████████
████████
████<#6ef514>█<#6ef514>███
████<#6ef514>█<#6ef514>█<#6ef514>██
████<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
<#6f1515>█<#6f1515>█<#6f1515>█<#6f1515>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
<#6f1515>█<#6f1515>█<#6f1515>█<#6f1515>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
"; + + public override bool IsEnable => On; + + private static OptionItem TreeSpriteVisible; + public static OptionItem FallDelay; + private static OptionItem AbilityUseLimit; + private static OptionItem AbilityUseGainWithEachTaskCompleted; + private static OptionItem AbilityChargesWhenFinishedTasks; + private static OptionItem FallRadius; + private static OptionItem FallKillChance; + public static OptionItem FallStunDuration; + + public bool TreeSpriteActive; + + public override void SetupCustomOption() + { + StartSetup(655100) + .AutoSetupOption(ref TreeSpriteVisible, true) + .AutoSetupOption(ref FallDelay, 5, new IntegerValueRule(1, 60, 1), OptionFormat.Seconds) + .AutoSetupOption(ref AbilityUseLimit, 1f, new FloatValueRule(0, 20, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityUseGainWithEachTaskCompleted, 0.5f, new FloatValueRule(0f, 5f, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityChargesWhenFinishedTasks, 0.2f, new FloatValueRule(0f, 5f, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref FallRadius, 3f, new FloatValueRule(0.25f, 10f, 0.25f), OptionFormat.Multiplier) + .AutoSetupOption(ref FallKillChance, 10, new IntegerValueRule(0, 100, 5), OptionFormat.Percent) + .AutoSetupOption(ref FallStunDuration, 5, new IntegerValueRule(1, 60, 1), OptionFormat.Seconds); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + playerId.SetAbilityUseLimit(AbilityUseLimit.GetFloat()); + TreeSpriteActive = false; + } + + public override void OnPet(PlayerControl pc) + { + if (pc.GetAbilityUseLimit() < 1) return; + pc.RpcRemoveAbilityUse(); + + Main.AllPlayerSpeed[pc.PlayerId] = Main.MinSpeed; + pc.MarkDirtySettings(); + + bool treeSpriteVisible = TreeSpriteVisible.GetBool(); + if (treeSpriteVisible) TreeSpriteActive = true; + else pc.RpcMakeInvisible(); + + LateTask.New(() => + { + if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || pc == null || !pc.IsAlive()) return; + + Main.AllPlayerSpeed[pc.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); + pc.MarkDirtySettings(); + + if (treeSpriteVisible) TreeSpriteActive = false; + else pc.RpcMakeVisible(); + + int chance = FallKillChance.GetInt(); + + Utils.GetPlayersInRadius(FallRadius.GetFloat(), pc.Pos()).Without(pc).Do(x => + { + if (IRandom.Instance.Next(100) < chance) x.Suicide(PlayerState.DeathReason.Fall, pc); + else + { + Main.AllPlayerSpeed[x.PlayerId] = Main.MinSpeed; + x.MarkDirtySettings(); + + LateTask.New(() => + { + if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || pc == null || !pc.IsAlive()) return; + Main.AllPlayerSpeed[x.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); + x.MarkDirtySettings(); + }, FallStunDuration.GetFloat(), log: false); + } + }); + + var cno = new FallenTree(pc.Pos()); + LateTask.New(() => cno.Despawn(), FallStunDuration.GetFloat(), log: false); + }, FallDelay.GetFloat(), log: false); + } +} \ No newline at end of file diff --git a/Roles/Impostor/AntiAdminer.cs b/Roles/Impostor/AntiAdminer.cs index c358acce8..4a09dcdcc 100644 --- a/Roles/Impostor/AntiAdminer.cs +++ b/Roles/Impostor/AntiAdminer.cs @@ -76,7 +76,7 @@ internal class AntiAdminer : RoleBase IsMonitor = Main.PlayerStates[playerId].MainRole == CustomRoles.Monitor; ExtraAbilityStartTimeStamp = 0; AntiAdminerId = playerId; - if (IsMonitor && Main.CurrentMap != MapNames.MiraHQ) playerId.SetAbilityUseLimit(Monitor.UseLimitOpt.GetInt()); + if (IsMonitor && Main.CurrentMap != MapNames.MiraHQ) playerId.SetAbilityUseLimit(Monitor.UseLimitOpt.GetFloat()); } public override void Remove(byte playerId) diff --git a/Roles/Impostor/Postponer.cs b/Roles/Impostor/Postponer.cs new file mode 100644 index 000000000..fcc01890e --- /dev/null +++ b/Roles/Impostor/Postponer.cs @@ -0,0 +1,63 @@ +using EHR.Modules; +using UnityEngine; + +namespace EHR.Impostor; + +public class Postponer : RoleBase +{ + public static bool On; + + public override bool IsEnable => On; + + private static OptionItem KillCooldown; + private static OptionItem BodyDelay; + + public override void SetupCustomOption() + { + StartSetup(655000) + .AutoSetupOption(ref KillCooldown, 30f, new FloatValueRule(0.5f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref BodyDelay, 10, new IntegerValueRule(1, 120, 1), OptionFormat.Seconds); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + } + + public override void SetKillCooldown(byte id) + { + Main.AllPlayerKillCooldown[id] = KillCooldown.GetFloat(); + } + + public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) + { + if (!base.OnCheckMurder(killer, target) || !killer.RpcCheckAndMurder(target, check: true)) return false; + + RPC.PlaySoundRPC(killer.PlayerId, Sounds.KillSound); + killer.RpcRemoveAbilityUse(); + target.Data.IsDead = true; + target.SetRealKiller(killer); + Main.PlayerStates[target.PlayerId].deathReason = PlayerState.DeathReason.Kill; + target.RpcExileV2(); + Main.PlayerStates[target.PlayerId].SetDead(); + Utils.AfterPlayerDeathTasks(target); + target.SetRealKiller(killer); + killer.SetKillCooldown(KillCooldown.GetFloat()); + + Vector2 position = target.Pos(); + byte colorId = (byte)target.Data.DefaultOutfit.ColorId; + + LateTask.New(() => + { + if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks) return; + Utils.RpcCreateDeadBody(position, colorId, target); + }, BodyDelay.GetInt(), "Postponer Body Delay"); + + return false; + } +} \ No newline at end of file From 5cda61610cde0c7c59cf6426503e0fc27d479282 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Mon, 15 Sep 2025 15:14:26 +0200 Subject: [PATCH 18/24] add strings --- Resources/Lang/en_US.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 9183f07f3..2a1723f8d 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -8999,6 +8999,14 @@ "Postponer": "Postponer", "PostponerInfo": "Postpone Dead Body Spawning", "PostponerInfoLong": "(Impostors):\\nAs the Postponer, when you kill, no dead body spawns instantly. Instead, it will spawn after a delay (in the same place as the murder).", + "Tree": "Tree", + "TreeInfo": "You are a Tree", + "TreeInfoLong": "(Crewmates):\\nAs a Tree, you can pet to act like one (become anonymous and invisible). After a few seconds, you will fall down, killing or stunning everyone nearby, and making you visible again.", + "Tree.TreeSpriteVisible": "Tree Sprite Visible", + "Tree.FallDelay": "Fall Delay", + "Tree.FallRadius": "Falling Effect Radius", + "Tree.FallKillChance": "Fall Kill Chance", + "Tree.FallStunDuration": "Fall Stun Duration", "Clairvoyant": "Clairvoyant", "ClairvoyantInfo": "Know Remaining Killer Counts", "ClairvoyantInfoLong": "(Crewmates):\\nAs the Clairvoyant, you can see the remaining impostor and neutral killer counts by entering a vent or petting.", From 0995d7c966806c1c2e9804af947357bd189a3ad1 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Wed, 17 Sep 2025 10:02:02 +0200 Subject: [PATCH 19/24] Add Ambusher --- CustomRoles.cs | 1 + GameMode/BedWars.cs | 17 ++- GameMode/SoloPVP.cs | 8 +- Modules/CustomNetObject.cs | 25 +--- Modules/CustomRolesHelper.cs | 53 +++++++- Modules/ExtendedPlayerControl.cs | 2 +- Modules/GuessManager.cs | 45 +++---- Modules/Utils.cs | 1 + Patches/PlayerControlPatch.cs | 30 ++--- Resources/Lang/en_US.json | 9 ++ Roles/Coven/Siren.cs | 2 + Roles/Impostor/Ambusher.cs | 205 +++++++++++++++++++++++++++++++ Roles/Impostor/BountyHunter.cs | 2 +- Roles/Impostor/Mercenary.cs | 29 ++++- Roles/Neutral/Slenderman.cs | 13 +- 15 files changed, 358 insertions(+), 84 deletions(-) create mode 100644 Roles/Impostor/Ambusher.cs diff --git a/CustomRoles.cs b/CustomRoles.cs index e3d38adbc..bfa24a8af 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -20,6 +20,7 @@ public enum CustomRoles // Impostors Abyssbringer, + Ambusher, Anonymous, AntiAdminer, Sans, // Arrogance diff --git a/GameMode/BedWars.cs b/GameMode/BedWars.cs index fe076d583..a363cd99c 100644 --- a/GameMode/BedWars.cs +++ b/GameMode/BedWars.cs @@ -735,7 +735,7 @@ public static class BedWars if (Health <= 0 && pc != null && pc.IsAlive()) { - killer.KillFlash(); + if (killer != null) killer.KillFlash(); if (Main.GM.Value && AmongUsClient.Instance.AmHost) PlayerControl.LocalPlayer.KillFlash(); ChatCommands.Spectators.ToValidPlayers().Do(x => x.KillFlash()); @@ -1833,7 +1833,20 @@ public static class BedWars break; } - if (didDamage) bed.UpdateStatus(); + if (didDamage) + { + foreach (byte id in bed.Breaking) + { + var pc = id.GetPlayer(); + if (pc == null) continue; + + NameNotifyManager.Notifies.Remove(pc.PlayerId); + Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); + } + + bed.Breaking.Clear(); + bed.UpdateStatus(); + } } } } diff --git a/GameMode/SoloPVP.cs b/GameMode/SoloPVP.cs index 44c144ad5..9188be43f 100644 --- a/GameMode/SoloPVP.cs +++ b/GameMode/SoloPVP.cs @@ -210,7 +210,7 @@ internal static class SoloPVP if (!target.SoloAlive()) { - OnPlayerDead(target); + Main.Instance.StartCoroutine(OnPlayerDead(target)); OnPlayerKill(killer); } @@ -238,10 +238,12 @@ internal static class SoloPVP Utils.NotifyRoles(SpecifyTarget: pc, SendOption: SendOption.None); } - private static void OnPlayerDead(PlayerControl target) + private static System.Collections.IEnumerator OnPlayerDead(PlayerControl target) { - target.ExileTemporarily(); BackCountdown.TryAdd(target.PlayerId, KB_ResurrectionWaitingTime.GetInt()); + if (target.inVent || target.MyPhysics.Animations.IsPlayingEnterVentAnimation()) LateTask.New(() => target.MyPhysics.RpcExitVent(target.GetClosestVent().Id), 0.6f, log: false); + while (target.inVent || target.inMovingPlat || target.onLadder || target.MyPhysics.Animations.IsPlayingAnyLadderAnimation() || target.MyPhysics.Animations.IsPlayingEnterVentAnimation()) yield return null; + if (BackCountdown.ContainsKey(target.PlayerId)) target.ExileTemporarily(); } private static void OnPlayerKill(PlayerControl killer) diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs index cee4d87f6..9554a41d3 100644 --- a/Modules/CustomNetObject.cs +++ b/Modules/CustomNetObject.cs @@ -692,34 +692,11 @@ namespace EHR internal sealed class ShapeshiftMenuElement : CustomNetObject { - private readonly byte VisibleTo; - - public ShapeshiftMenuElement(string display, string namePlate, byte visibleTo) + public ShapeshiftMenuElement(byte visibleTo) { - VisibleTo = visibleTo; CreateNetObject(string.Empty, new Vector2(0f, 0f)); - LateTask.New(() => SetData(display, namePlate), 1f, log: false); Main.AllPlayerControls.DoIf(x => x.PlayerId != visibleTo, Hide); } - - public void SetData(string display, string namePlate) - { - Logger.Warn(playerControl.Data == null ? "playerControl.Data is null" : "playerControl.Data is not null", "ShapeshiftMenuElement"); - playerControl.Data.DefaultOutfit.NamePlateId = namePlate; - playerControl.Data.PlayerName = $"{Translator.GetString(display).ToUpper()}{display}"; - - MessageWriter writer = MessageWriter.Get(SendOption.Reliable); - writer.StartMessage(6); - writer.Write(AmongUsClient.Instance.GameId); - writer.WritePacked(VisibleTo.GetPlayer().OwnerId); - writer.StartMessage(1); - writer.WritePacked(playerControl.Data.NetId); - playerControl.Data.Serialize(writer, false); - writer.EndMessage(); - writer.EndMessage(); - AmongUsClient.Instance.SendOrDisconnect(writer); - writer.Recycle(); - } } } diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 3449f987d..9154b9476 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -108,9 +108,9 @@ internal static class CustomRolesHelper if (role.IsVanilla()) return role; if (role is CustomRoles.GM) return CustomRoles.Crewmate; if (checkDesyncRole && role.IsDesyncRole()) return Enum.Parse(role.GetDYRole() + "EHR"); + if ((Options.UsePhantomBasis.GetBool() || role.AlwaysUsesPhantomBase()) && role.SimpleAbilityTrigger()) return CustomRoles.Phantom; bool UsePets = Options.UsePets.GetBool(); - if ((Options.UsePhantomBasis.GetBool() || role.AlwaysUsesPhantomBase()) && role.SimpleAbilityTrigger() && (!UsePets || role is CustomRoles.Swooper or CustomRoles.Wraith)) return CustomRoles.Phantom; return role switch { @@ -153,8 +153,9 @@ internal static class CustomRolesHelper CustomRoles.ClockBlocker => CustomRoles.Impostor, CustomRoles.Psychopath => CustomRoles.Impostor, CustomRoles.Postponer => CustomRoles.Impostor, - CustomRoles.Venerer => CustomRoles.Shapeshifter, + CustomRoles.Venerer => UsePets ? CustomRoles.Impostor : CustomRoles.Shapeshifter, CustomRoles.Kidnapper => CustomRoles.Shapeshifter, + CustomRoles.Ambusher => UsePets ? CustomRoles.Impostor : CustomRoles.Shapeshifter, CustomRoles.Stasis => UsePets ? CustomRoles.Impostor : CustomRoles.Shapeshifter, CustomRoles.Wasp => CustomRoles.Impostor, CustomRoles.Assumer => CustomRoles.Impostor, @@ -433,8 +434,9 @@ internal static class CustomRolesHelper public static RoleTypes GetDYRole(this CustomRoles role, bool load = false) { + if (!load && ((Options.UsePhantomBasis.GetBool() && Options.UsePhantomBasisForNKs.GetBool()) || role.AlwaysUsesPhantomBase()) && !role.IsImpostor() && role.SimpleAbilityTrigger()) return RoleTypes.Phantom; + bool UsePets = !load && Options.UsePets.GetBool(); - if (!load && ((Options.UsePhantomBasis.GetBool() && Options.UsePhantomBasisForNKs.GetBool()) || role.AlwaysUsesPhantomBase()) && !role.IsImpostor() && role.SimpleAbilityTrigger() && (!UsePets || role is CustomRoles.Swooper or CustomRoles.Wraith)) return RoleTypes.Phantom; return role switch { @@ -713,6 +715,7 @@ internal static class CustomRolesHelper CustomRoles.Inhibitor or CustomRoles.Wiper or CustomRoles.Psychopath or + CustomRoles.Ambusher or CustomRoles.Kidnapper or CustomRoles.Postponer or CustomRoles.Venerer or @@ -848,6 +851,8 @@ internal static class CustomRolesHelper if (role.UsesPetInsteadOfKill()) return true; + if (Options.UsePhantomBasis.GetBool() && (!role.IsNK() || Options.UsePhantomBasisForNKs.GetBool()) && role.SimpleAbilityTrigger()) return false; + Type type = role.GetRoleClass().GetType(); return type.GetMethod("OnPet")?.DeclaringType == type; } @@ -1024,6 +1029,7 @@ internal static class CustomRolesHelper CustomRoles.Swiftclaw or CustomRoles.Undertaker or CustomRoles.Abyssbringer or + CustomRoles.Ambusher or CustomRoles.Bomber or CustomRoles.Nuker or CustomRoles.Camouflager or @@ -1039,9 +1045,9 @@ internal static class CustomRolesHelper CustomRoles.Sapper or CustomRoles.Sniper or CustomRoles.Twister or - CustomRoles.Swooper or //! + CustomRoles.Swooper or CustomRoles.Venerer or - CustomRoles.Wraith or //! + CustomRoles.Wraith or CustomRoles.RouleteGrandeur or CustomRoles.Enderman or CustomRoles.Explosivist or @@ -1244,7 +1250,41 @@ internal static class CustomRolesHelper CustomRoles.Eraser when Eraser.EraseMethod.GetValue() == 0 => true, CustomRoles.Silencer when Silencer.SilenceMode.GetValue() == 0 => true, CustomRoles.Sniper when !Sniper.CanKillWithBullets.GetBool() => true, - CustomRoles.Augmenter or CustomRoles.BallLightning or CustomRoles.Blackmailer or CustomRoles.Cantankerous or CustomRoles.Capitalism or CustomRoles.Consort or CustomRoles.Echo or CustomRoles.EvilDiviner or CustomRoles.FireWorks or CustomRoles.Framer or CustomRoles.Gangster or CustomRoles.Generator or CustomRoles.Hangman or CustomRoles.Inhibitor or CustomRoles.Kamikaze or CustomRoles.Mastermind or CustomRoles.Morphling or CustomRoles.Ninja or CustomRoles.Nullifier or CustomRoles.Penguin or CustomRoles.Puppeteer or CustomRoles.Saboteur or CustomRoles.Sapper or CustomRoles.Scavenger or CustomRoles.Swooper or CustomRoles.Undertaker or CustomRoles.Vampire or CustomRoles.Warlock or CustomRoles.Wasp or CustomRoles.Wiper or CustomRoles.Witch or CustomRoles.YinYanger => true, + + CustomRoles.Augmenter or + CustomRoles.BallLightning or + CustomRoles.Blackmailer or + CustomRoles.Cantankerous or + CustomRoles.Capitalism or + CustomRoles.Consort or + CustomRoles.Echo or + CustomRoles.EvilDiviner or + CustomRoles.FireWorks or + CustomRoles.Framer or + CustomRoles.Gangster or + CustomRoles.Generator or + CustomRoles.Hangman or + CustomRoles.Inhibitor or + CustomRoles.Kamikaze or + CustomRoles.Mastermind or + CustomRoles.Morphling or + CustomRoles.Ninja or + CustomRoles.Nullifier or + CustomRoles.Penguin or + CustomRoles.Postponer or + CustomRoles.Puppeteer or + CustomRoles.Saboteur or + CustomRoles.Sapper or + CustomRoles.Scavenger or + CustomRoles.Swooper or + CustomRoles.Undertaker or + CustomRoles.Vampire or + CustomRoles.Warlock or + CustomRoles.Wasp or + CustomRoles.Wiper or + CustomRoles.Witch or + CustomRoles.YinYanger => true, + _ => false }; } @@ -1621,6 +1661,7 @@ internal static class CustomRolesHelper CustomRoles.Ventriloquist => RoleOptionType.Impostor_Support, CustomRoles.Vindicator => RoleOptionType.Impostor_Support, CustomRoles.YinYanger => RoleOptionType.Impostor_Support, + CustomRoles.Ambusher => RoleOptionType.Impostor_Concealing, CustomRoles.Anonymous => RoleOptionType.Impostor_Concealing, CustomRoles.Duellist => RoleOptionType.Impostor_Concealing, CustomRoles.Echo => RoleOptionType.Impostor_Concealing, diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 7ea81eead..97ad27731 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -733,7 +733,7 @@ internal static class ExtendedPlayerControl public static void KillFlash(this PlayerControl player) { - if (GameStates.IsLobby) return; + if (GameStates.IsLobby || player == null) return; // Kill flash (blackout + reactor flash) processing diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs index aa9144a5e..7f75a0223 100644 --- a/Modules/GuessManager.cs +++ b/Modules/GuessManager.cs @@ -1267,7 +1267,7 @@ public static class GuessManager bool TryGetDisplay(out string display) { if (!PlayerIdToRawDisplay.TryGetValue(target.PlayerId, out display)) //TODO: ANY WAY TO FIND CNOS FROM PLAYERCONTROL? - display = ExistingCNOs.FindFirst(x => x.playerControl.name == target.name, out ShapeshiftMenuElement cno) ? cno.playerControl.tag : string.Empty; + display = ExistingCNOs.FindFirst(x => x.playerControl.NetId == target.NetId, out ShapeshiftMenuElement cno) ? cno.playerControl.tag : string.Empty; if (string.IsNullOrEmpty(display)) { @@ -1315,6 +1315,8 @@ public static class GuessManager // First, use living players to show choices by changing their names // The local player can't be used to show a choice (-1) + StringBuilder sb = new(); + var skipped = false; PlayerControl guesser = GuesserId.GetPlayer(); MessageWriter writer = MessageWriter.Get(SendOption.Reliable); @@ -1338,9 +1340,11 @@ public static class GuessManager string originalNamePlateId = pc.Data.DefaultOutfit.NamePlateId; string originalPlayerName = pc.Data.DefaultOutfit.PlayerName; - pc.Data.DefaultOutfit.NamePlateSequenceId += 10; pc.Data.DefaultOutfit.NamePlateId = namePlateId; + pc.SetNamePlate(namePlateId); + pc.Data.DefaultOutfit.NamePlateSequenceId += 10; pc.Data.DefaultOutfit.PlayerName = GetString(choice).ToUpper(); + sb.Append(pc.Data.DefaultOutfit.PlayerName.PadRightV2(10)); if (writer.Length > 500) { @@ -1357,6 +1361,13 @@ public static class GuessManager pc.Data.Serialize(writer, false); writer.EndMessage(); + writer.StartMessage(2); + writer.WritePacked(pc.NetId); + writer.Write((byte)RpcCalls.SetNamePlateStr); + writer.Write(namePlateId); + writer.Write(pc.GetNextRpcSequenceId(RpcCalls.SetNamePlateStr)); + writer.EndMessage(); + pc.Data.DefaultOutfit.NamePlateId = originalNamePlateId; pc.Data.DefaultOutfit.PlayerName = originalPlayerName; } @@ -1369,40 +1380,22 @@ public static class GuessManager ExistingCNOs.Clear(); // If there aren't enough living players, spawn new CNOs to show the rest of choices + + // Since CNOs use the local player's NetworkedPlayerInfo, add AU reads the player's name directly from it, + // it's impossible to show vanilla players all choices accurately with CNOs. + // No workaround found yet.... + // So we send the remaining choices in chat so the player can identify them if (data.Length >= alivePlayerControlsLength) { - string originalNamePlateId = guesser.Data.DefaultOutfit.NamePlateId; - string originalPlayerName = guesser.Data.DefaultOutfit.PlayerName; - for (int i = alivePlayerControlsLength; i < data.Length; i++) { string choice = data[i].choice; string namePlateId = data[i].namePlateId; - guesser.Data.DefaultOutfit.NamePlateSequenceId += 10; - guesser.Data.DefaultOutfit.NamePlateId = namePlateId; - guesser.Data.DefaultOutfit.PlayerName = $"{GetString(choice).ToUpper()}{choice}"; - - writer = MessageWriter.Get(SendOption.Reliable); - writer.StartMessage(6); - writer.Write(AmongUsClient.Instance.GameId); - writer.WritePacked(guesser.OwnerId); - writer.StartMessage(1); - writer.WritePacked(guesser.Data.NetId); - guesser.Data.Serialize(writer, false); - writer.EndMessage(); - writer.EndMessage(); - AmongUsClient.Instance.SendOrDisconnect(writer); - writer.Recycle(); - - var cno = new ShapeshiftMenuElement(choice, namePlateId, GuesserId); + var cno = new ShapeshiftMenuElement(GuesserId); ExistingCNOs.Add(cno); } - - guesser.Data.DefaultOutfit.NamePlateSequenceId += 10; - guesser.Data.DefaultOutfit.NamePlateId = originalNamePlateId; - guesser.Data.DefaultOutfit.PlayerName = originalPlayerName; } Logger.Info($"Spawned {ExistingCNOs.Count} CNOs, Used {alivePlayerControlsLength} Living Players, Showing {data.Length} Choices", "Meeting Shapeshift For Guessing"); diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 93e10c51b..428cd7f88 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -3258,6 +3258,7 @@ public static class Utils CustomRoles.Catcher => Catcher.AbilityCooldown.GetInt(), CustomRoles.Sentry => Crewmate.Sentry.ShowInfoCooldown.GetInt(), CustomRoles.ToiletMaster => ToiletMaster.AbilityCooldown.GetInt(), + CustomRoles.Ambusher => Ambusher.AbilityCooldown.GetInt(), CustomRoles.AntiAdminer => AntiAdminer.AbilityCooldown.GetInt(), CustomRoles.Sniper => Options.DefaultShapeshiftCooldown.GetInt(), CustomRoles.Ninja => Ninja.AssassinateCooldownOpt.GetInt(), diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index fd9a99d99..07a7a9e49 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -351,7 +351,7 @@ internal static class CheckMurderPatch return false; } - bool CheckMurder() => Main.PlayerStates[killer.PlayerId].Role.OnCheckMurder(killer, target) || target.Is(CustomRoles.Fragile); + bool CheckMurder() => Main.PlayerStates[killer.PlayerId].Role.OnCheckMurder(killer, target) || target.Is(CustomRoles.Fragile) || Ambusher.FragilePlayers.ContainsKey(target.PlayerId); } if (!killer.RpcCheckAndMurder(target, true)) return false; @@ -1300,6 +1300,20 @@ internal static class ReportDeadBodyPatch } } + foreach (PlayerControl pc in Main.AllPlayerControls) + { + if (Main.CheckShapeshift.ContainsKey(pc.PlayerId) && !Doppelganger.DoppelVictim.ContainsKey(pc.PlayerId)) + Camouflage.RpcSetSkin(pc, revertToDefault: true); + + if (Main.CurrentMap == MapNames.Fungle && (pc.IsMushroomMixupActive() || IsActive(SystemTypes.MushroomMixupSabotage))) + pc.FixMixedUpOutfit(); + + if (Main.Invisible.Contains(pc.PlayerId)) + pc.RpcMakeVisible(); + + PhantomRolePatch.OnReportDeadBody(pc); + } + EAC.TimeSinceLastTaskCompletion.Clear(); Enigma.OnReportDeadBody(player, target); @@ -1342,20 +1356,6 @@ internal static class ReportDeadBodyPatch Stressed.OnMeetingStart(); - foreach (PlayerControl pc in Main.AllPlayerControls) - { - if (Main.CheckShapeshift.ContainsKey(pc.PlayerId) && !Doppelganger.DoppelVictim.ContainsKey(pc.PlayerId)) - Camouflage.RpcSetSkin(pc, revertToDefault: true); - - if (Main.CurrentMap == MapNames.Fungle && (pc.IsMushroomMixupActive() || IsActive(SystemTypes.MushroomMixupSabotage))) - pc.FixMixedUpOutfit(); - - if (Main.Invisible.Contains(pc.PlayerId)) - pc.RpcMakeVisible(); - - PhantomRolePatch.OnReportDeadBody(pc); - } - MeetingTimeManager.OnReportDeadBody(); NameNotifyManager.Reset(); diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 2a1723f8d..67ebfc15c 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -9262,6 +9262,15 @@ "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", "Listener.FromTo": "{0}\\n{1} whispers to {2}", + "Ambusher": "Ambusher", + "AmbusherInfo": "Ambush Players to Make Them Fragile", + "AmbusherInfoLong": "(Impostors):\\nAs the Ambusher, use your ability to become invisible. Follow someone for a few seconds to ambush them. If you succeed, the player gets Fragile effects. Otherwise, you become visible once your invisibility duration runs out and nothing happens.", + "Ambusher.FollowDuration": "Time Needed to Follow Someone For", + "Ambusher.FollowRadius": "Following Radius", + "Ambusher.InvisDurAfterSuccessfulAmbush": "Extra Invisibility Duration After Successful Ambushes", + "Ambusher.FragileDuration": "Fragile Effect Duration", + "Ambusher.Success": "<#00ff00>Ambush successful", + "Ambusher.InProgress": "Ambushing {0}.... {1}s left", "Psychopath": "Psychopath", "PsychopathInfo": "You're Forced to Kill Off Cooldown", "PsychopathInfoLong": "(Impostors):\\nAs the Psychopath, as soon as your kill cooldown is up, you'll kill instantly if someone is inside your kill-range.", diff --git a/Roles/Coven/Siren.cs b/Roles/Coven/Siren.cs index 1e25745cf..c652a89c7 100644 --- a/Roles/Coven/Siren.cs +++ b/Roles/Coven/Siren.cs @@ -123,6 +123,8 @@ public class Siren : Coven { if (!Stages.TryGetValue(player.PlayerId, out var stage)) Stages[player.PlayerId] = stage = HasNecronomicon ? 2 : 1; + else + Stages[player.PlayerId]++; switch (stage) { diff --git a/Roles/Impostor/Ambusher.cs b/Roles/Impostor/Ambusher.cs new file mode 100644 index 000000000..be8c3e1ac --- /dev/null +++ b/Roles/Impostor/Ambusher.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; +using EHR.Modules; +using Hazel; +using UnityEngine; + +namespace EHR.Impostor; + +public class Ambusher : RoleBase +{ + public static bool On; + + public override bool IsEnable => On; + + public static OptionItem AbilityCooldown; + private static OptionItem AbilityDuration; + private static OptionItem KillCooldown; + private static OptionItem FollowDuration; + private static OptionItem FollowRadius; + private static OptionItem InvisDurAfterSuccessfulAmbush; + private static OptionItem FragileDuration; + + private float AbilityEndTimer; + private byte TargetId; + private float TargetTimer; + private bool DontCheck; + private int Count; + private long LastRPCTS; + private byte AmbusherId; + + public static Dictionary FragilePlayers = []; + + public override void SetupCustomOption() + { + StartSetup(655200) + .AutoSetupOption(ref AbilityCooldown, 30f, new FloatValueRule(0.5f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref AbilityDuration, 20f, new FloatValueRule(0.5f, 60f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref KillCooldown, 30f, new FloatValueRule(0.5f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref FollowDuration, 5f, new FloatValueRule(0.5f, 30f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref FollowRadius, 1.5f, new FloatValueRule(0.1f, 10f, 0.1f), OptionFormat.Multiplier) + .AutoSetupOption(ref InvisDurAfterSuccessfulAmbush, 5f, new FloatValueRule(0.5f, 60f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref FragileDuration, 40, new IntegerValueRule(1, 300, 1), OptionFormat.Seconds); + } + + public override void Init() + { + On = false; + FragilePlayers = []; + } + + public override void Add(byte playerId) + { + On = true; + AbilityEndTimer = AbilityDuration.GetFloat(); + TargetId = byte.MaxValue; + TargetTimer = FollowDuration.GetFloat(); + DontCheck = false; + Count = 0; + LastRPCTS = 0; + AmbusherId = playerId; + } + + public override void ApplyGameOptions(IGameOptions opt, byte playerId) + { + if (Options.UsePhantomBasis.GetBool()) + { + AURoleOptions.PhantomCooldown = AbilityCooldown.GetFloat(); + AURoleOptions.PhantomDuration = 1f; + } + else + { + AURoleOptions.ShapeshifterCooldown = AbilityCooldown.GetFloat(); + AURoleOptions.ShapeshifterDuration = 1f; + } + } + + public override bool CanUseKillButton(PlayerControl pc) + { + return !Main.Invisible.Contains(pc.PlayerId); + } + + public override void SetKillCooldown(byte id) + { + Main.AllPlayerKillCooldown[id] = KillCooldown.GetFloat(); + } + + public override void OnMurder(PlayerControl killer, PlayerControl target) + { + killer.RpcResetAbilityCooldown(); + } + + public override bool OnVanish(PlayerControl pc) + { + pc.RpcMakeInvisible(true); + return false; + } + + public override bool OnShapeshift(PlayerControl shapeshifter, PlayerControl target, bool shapeshifting) + { + if (!shapeshifting) return true; + shapeshifter.RpcMakeInvisible(true); + return false; + } + + public override void OnPet(PlayerControl pc) + { + pc.RpcMakeInvisible(true); + } + + public override void OnFixedUpdate(PlayerControl pc) + { + if (!GameStates.IsInTask || !Main.IntroDestroyed || ExileController.Instance || AntiBlackout.SkipTasks || !Main.Invisible.Contains(pc.PlayerId)) return; + + if (!DontCheck) + { + var pos = pc.Pos(); + var radius = FollowRadius.GetFloat(); + var closestPlayer = Main.AllAlivePlayerControls.Without(pc).Select(x => (pc: x, distance: Vector2.Distance(x.Pos(), pos))).Where(x => x.distance <= radius).MinBy(x => x.distance).pc; + + if (closestPlayer == null) + { + TargetId = byte.MaxValue; + TargetTimer = FollowDuration.GetFloat(); + } + else if (closestPlayer.PlayerId != TargetId) + { + TargetId = closestPlayer.PlayerId; + TargetTimer = FollowDuration.GetFloat(); + } + else + { + TargetTimer -= Time.fixedDeltaTime; + + if (TargetTimer <= 0) + { + DontCheck = true; + AbilityEndTimer = InvisDurAfterSuccessfulAmbush.GetFloat(); + FragilePlayers[TargetId] = Utils.TimeStamp + FragileDuration.GetInt(); + TargetId = byte.MaxValue; + TargetTimer = FollowDuration.GetFloat(); + } + } + } + + AbilityEndTimer -= Time.fixedDeltaTime; + + if (AbilityEndTimer <= 0) + { + pc.SetKillCooldown(KillCooldown.GetFloat()); + pc.RpcMakeVisible(true); + AbilityEndTimer = AbilityDuration.GetFloat(); + LastRPCTS = 0; + Count = 30; + DontCheck = false; + } + + if (LastRPCTS != Utils.TimeStamp) + { + pc.RpcResetAbilityCooldown(); + Utils.SendRPC(CustomRPC.SyncRoleData, AmbusherId, TargetId, TargetTimer, AbilityEndTimer, DontCheck, AmbusherId, Main.Invisible.Contains(AmbusherId)); + LastRPCTS = Utils.TimeStamp; + } + + if (Count++ < (DontCheck ? 30 : 10)) return; + Count = 0; + + Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); + if (pc.AmOwner) Utils.DirtyName.Add(pc.PlayerId); + } + + public override void OnGlobalFixedUpdate(PlayerControl pc, bool lowLoad) + { + if (!lowLoad && FragilePlayers.TryGetValue(pc.PlayerId, out var endTS) && Utils.TimeStamp >= endTS) + FragilePlayers.Remove(pc.PlayerId); + } + + public override void OnReportDeadBody() + { + TargetId = byte.MaxValue; + TargetTimer = FollowDuration.GetFloat(); + AbilityEndTimer = AbilityDuration.GetFloat(); + DontCheck = false; + Utils.SendRPC(CustomRPC.SyncRoleData, AmbusherId, TargetId, TargetTimer, AbilityEndTimer, DontCheck, AmbusherId, Main.Invisible.Contains(AmbusherId)); + } + + public void ReceiveRPC(MessageReader reader) + { + TargetId = reader.ReadByte(); + TargetTimer = reader.ReadSingle(); + AbilityEndTimer = reader.ReadSingle(); + DontCheck = reader.ReadBoolean(); + byte ambusherId = reader.ReadByte(); + if (reader.ReadBoolean()) Main.Invisible.Add(ambusherId); + else Main.Invisible.Remove(ambusherId); + } + + public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) + { + if (seer.PlayerId != AmbusherId || seer.PlayerId != target.PlayerId || hud || meeting || !Main.Invisible.Contains(AmbusherId)) return string.Empty; + string str = DontCheck ? Translator.GetString("Ambusher.Success") : string.Format(Translator.GetString("Ambusher.InProgress"), TargetId.ColoredPlayerName(), Math.Round(TargetTimer, 1)); + return $"{str}\n{string.Format(Translator.GetString("Ambusher.InvisTimeLeft"), (int)Math.Round(AbilityEndTimer))}"; + } +} \ No newline at end of file diff --git a/Roles/Impostor/BountyHunter.cs b/Roles/Impostor/BountyHunter.cs index 54c9cb80d..0f8dc901c 100644 --- a/Roles/Impostor/BountyHunter.cs +++ b/Roles/Impostor/BountyHunter.cs @@ -125,7 +125,7 @@ public class BountyHunter : RoleBase public override void OnFixedUpdate(PlayerControl player) { - if (!GameStates.IsInTask || float.IsNaN(ChangeTimer)) return; + if (!GameStates.IsInTask || float.IsNaN(ChangeTimer) || !Main.IntroDestroyed || ExileController.Instance || AntiBlackout.SkipTasks) return; if (!player.IsAlive()) ChangeTimer = float.NaN; diff --git a/Roles/Impostor/Mercenary.cs b/Roles/Impostor/Mercenary.cs index 1b32a3007..7de44f1ed 100644 --- a/Roles/Impostor/Mercenary.cs +++ b/Roles/Impostor/Mercenary.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using EHR.Modules; +using Hazel; using UnityEngine; namespace EHR.Impostor; @@ -15,6 +16,7 @@ public class Mercenary : RoleBase private float SuicideTimer; private int Timer; + private byte MercenaryId; public override bool IsEnable => PlayerIdList.Count > 0; @@ -37,7 +39,7 @@ public class Mercenary : RoleBase public override void Init() { PlayerIdList = []; - SuicideTimer = TimeLimit.GetFloat(); + SuicideTimer = 0f; Timer = TimeLimit.GetInt(); } @@ -45,7 +47,8 @@ public class Mercenary : RoleBase { PlayerIdList.Add(serial); Timer = TimeLimit.GetInt(); - SuicideTimer = TimeLimit.GetFloat(); + SuicideTimer = 0f; + MercenaryId = serial; } public override void Remove(byte playerId) @@ -67,7 +70,7 @@ public class Mercenary : RoleBase { if (!killer.Is(CustomRoles.Mercenary)) return true; - SuicideTimer = float.NaN; + SuicideTimer = 0f; Timer = TimeLimit.GetInt(); killer.MarkDirtySettings(); return true; @@ -81,7 +84,7 @@ public class Mercenary : RoleBase public override void OnFixedUpdate(PlayerControl player) { - if (!GameStates.IsInTask) return; + if (!GameStates.IsInTask || !Main.IntroDestroyed || ExileController.Instance || AntiBlackout.SkipTasks) return; if (!HasKilled(player)) { @@ -106,7 +109,12 @@ public class Mercenary : RoleBase SuicideTimer += Time.fixedDeltaTime; int tempTimer = Timer; Timer = TimeLimit.GetInt() - (int)SuicideTimer; - if (Timer != tempTimer && Timer <= 20 && !player.IsModdedClient()) Utils.NotifyRoles(SpecifySeer: player, SpecifyTarget: player); + + if (Timer != tempTimer && Timer <= 20 && !player.IsModdedClient()) + { + Utils.NotifyRoles(SpecifySeer: player, SpecifyTarget: player); + Utils.SendRPC(CustomRPC.SyncRoleData, MercenaryId, Timer); + } } } @@ -115,4 +123,15 @@ public class Mercenary : RoleBase SuicideTimer = 0f; Timer = TimeLimit.GetInt(); } + + public void ReceiveRPC(MessageReader reader) + { + Timer = reader.ReadPackedInt32(); + } + + public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) + { + if (seer.PlayerId != MercenaryId || seer.PlayerId != target.PlayerId || (seer.IsModdedClient() && !hud) || meeting || Timer - 1 > 20) return string.Empty; + return string.Format(Translator.GetString("SerialKillerTimeLeft"), Timer - 1); + } } \ No newline at end of file diff --git a/Roles/Neutral/Slenderman.cs b/Roles/Neutral/Slenderman.cs index 92b2a2ba6..6797d0515 100644 --- a/Roles/Neutral/Slenderman.cs +++ b/Roles/Neutral/Slenderman.cs @@ -74,7 +74,13 @@ public class Slenderman : RoleBase public override void OnCheckPlayerPosition(PlayerControl pc) { - if (MeetingCooldownEndTS > Utils.TimeStamp || pc.Is(CustomRoles.Slenderman) || SlendermanPC == null || !SlendermanPC.IsAlive()) return; + if (pc.Is(CustomRoles.Slenderman)) return; + + if (MeetingCooldownEndTS > Utils.TimeStamp || SlendermanPC == null || !SlendermanPC.IsAlive()) + { + Blinded.Clear(); + return; + } bool inRange = Vector2.Distance(pc.Pos(), SlendermanPC.Pos()) <= BlindRange.GetFloat(); @@ -105,6 +111,11 @@ public class Slenderman : RoleBase MeetingCooldownEndTS = Utils.TimeStamp + AfterMeetingBlindCooldown.GetInt(); } + public override void OnReportDeadBody() + { + Blinded.Clear(); + } + public override string GetProgressText(byte playerId, bool comms) { return base.GetProgressText(playerId, comms) + $" ({Blinded.Count})"; From 7ef368f85d960aab2fe07e4813f1c0384fbc7ec9 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Wed, 17 Sep 2025 10:02:32 +0200 Subject: [PATCH 20/24] a --- Roles/Impostor/Ambusher.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Roles/Impostor/Ambusher.cs b/Roles/Impostor/Ambusher.cs index be8c3e1ac..e1adbaac6 100644 --- a/Roles/Impostor/Ambusher.cs +++ b/Roles/Impostor/Ambusher.cs @@ -113,6 +113,8 @@ public class Ambusher : RoleBase { if (!GameStates.IsInTask || !Main.IntroDestroyed || ExileController.Instance || AntiBlackout.SkipTasks || !Main.Invisible.Contains(pc.PlayerId)) return; + long now = Utils.TimeStamp; + if (!DontCheck) { var pos = pc.Pos(); @@ -137,7 +139,7 @@ public class Ambusher : RoleBase { DontCheck = true; AbilityEndTimer = InvisDurAfterSuccessfulAmbush.GetFloat(); - FragilePlayers[TargetId] = Utils.TimeStamp + FragileDuration.GetInt(); + FragilePlayers[TargetId] = now + FragileDuration.GetInt(); TargetId = byte.MaxValue; TargetTimer = FollowDuration.GetFloat(); } @@ -156,11 +158,11 @@ public class Ambusher : RoleBase DontCheck = false; } - if (LastRPCTS != Utils.TimeStamp) + if (LastRPCTS != now) { pc.RpcResetAbilityCooldown(); Utils.SendRPC(CustomRPC.SyncRoleData, AmbusherId, TargetId, TargetTimer, AbilityEndTimer, DontCheck, AmbusherId, Main.Invisible.Contains(AmbusherId)); - LastRPCTS = Utils.TimeStamp; + LastRPCTS = now; } if (Count++ < (DontCheck ? 30 : 10)) return; From 87312219ded53095772151bdfb5954a069494534 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Wed, 17 Sep 2025 11:01:28 +0200 Subject: [PATCH 21/24] a --- Modules/GuessManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs index 7f75a0223..fd7ba6e6a 100644 --- a/Modules/GuessManager.cs +++ b/Modules/GuessManager.cs @@ -300,7 +300,7 @@ public static class GuessManager case CustomRoles.Phantasm: ShowMessage("GuessPhantom"); return true; - case CustomRoles.Snitch when target.GetTaskState().RemainingTasksCount <= Snitch.RemainingTasksToBeFound: + case CustomRoles.Snitch when pc.IsSnitchTarget() && target.GetTaskState().RemainingTasksCount <= Snitch.RemainingTasksToBeFound: ShowMessage("EGGuessSnitchTaskDone"); return true; case CustomRoles.Merchant when Merchant.IsBribedKiller(pc, target): From d4108fad57b04cd77218083fea36c68613a315d2 Mon Sep 17 00:00:00 2001 From: PEPPERcula Date: Wed, 17 Sep 2025 23:27:55 -0700 Subject: [PATCH 22/24] Fix Examiner role color being the same as Detective (#357) * Update ControlPatch.cs * Update Main.cs * Update ChatCommandPatch.cs * Update ClientPatch.cs * Update ControlPatch.cs --- Main.cs | 5 +++-- Patches/ChatCommandPatch.cs | 4 ---- Patches/ClientPatch.cs | 3 --- Patches/ControlPatch.cs | 6 ++---- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Main.cs b/Main.cs index d4e7c9405..7b0d5fd1b 100644 --- a/Main.cs +++ b/Main.cs @@ -677,7 +677,7 @@ public class Main : BasePlugin { CustomRoles.Physicist, "#87e9ff" }, { CustomRoles.Finder, "#32a879" }, { CustomRoles.Noisy, "#e34fb2" }, - { CustomRoles.Examiner, "#625EEE" }, + { CustomRoles.Examiner, "#326ac9" }, { CustomRoles.Venom, "#ff1919" }, { CustomRoles.Torch, "#eee5be" }, { CustomRoles.Seer, "#61b26c" }, @@ -1167,4 +1167,5 @@ public enum TieMode Random } -public class Coroutines : MonoBehaviour { } \ No newline at end of file + +public class Coroutines : MonoBehaviour { } diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 0a5065345..16e870442 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -3836,7 +3836,3 @@ internal static class RpcSendChatPatch return false; } } - - - - diff --git a/Patches/ClientPatch.cs b/Patches/ClientPatch.cs index cc3c5ea4a..8db8d1279 100644 --- a/Patches/ClientPatch.cs +++ b/Patches/ClientPatch.cs @@ -211,6 +211,3 @@ internal static class AuthTimeoutPatch #endif } - - - diff --git a/Patches/ControlPatch.cs b/Patches/ControlPatch.cs index 33b325b24..5f1363825 100644 --- a/Patches/ControlPatch.cs +++ b/Patches/ControlPatch.cs @@ -55,9 +55,7 @@ internal static class ControllerManagerUpdatePatch else InGameRoleInfoMenu.Hide(); - bool admin = ChatCommands.IsPlayerAdmin(PlayerControl.LocalPlayer.FriendCode); - - if (KeysDown(KeyCode.Return, KeyCode.C, KeyCode.LeftShift) && admin) + if (KeysDown(KeyCode.Return, KeyCode.C, KeyCode.LeftShift)) HudManager.Instance.Chat.SetVisible(true); if (KeysDown(KeyCode.F11, KeyCode.LeftAlt)) @@ -92,6 +90,7 @@ internal static class ControllerManagerUpdatePatch if (KeysDown(KeyCode.LeftAlt, KeyCode.C) && !Input.GetKey(KeyCode.LeftShift) && !GameStates.IsNotJoined) Utils.CopyCurrentSettings(); + bool admin = ChatCommands.IsPlayerAdmin(PlayerControl.LocalPlayer.FriendCode); if (!AmongUsClient.Instance.AmHost && !admin) return; if (KeysDown(KeyCode.Return, KeyCode.L, KeyCode.LeftShift) && GameStates.IsInGame) @@ -454,4 +453,3 @@ public static class InGameRoleInfoMenu } } - From 76a425a3a03b681b35d1eb162a18f850f17f1ab1 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 18 Sep 2025 14:44:45 +0200 Subject: [PATCH 23/24] Add Captain --- CustomRoles.cs | 1 + Main.cs | 1 + Modules/CustomRolesHelper.cs | 3 + Modules/CustomSabotage.cs | 2 +- Resources/Lang/en_US.json | 5 ++ Roles/Crewmate/Captain.cs | 115 +++++++++++++++++++++++++++++++++++ Roles/Impostor/Psychopath.cs | 2 +- 7 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 Roles/Crewmate/Captain.cs diff --git a/CustomRoles.cs b/CustomRoles.cs index bfa24a8af..a5f9ff0bd 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -157,6 +157,7 @@ public enum CustomRoles Benefactor, Bodyguard, CameraMan, + Captain, Car, Catcher, Chameleon, diff --git a/Main.cs b/Main.cs index d4e7c9405..864c328fb 100644 --- a/Main.cs +++ b/Main.cs @@ -417,6 +417,7 @@ public class Main : BasePlugin { CustomRoles.Gardener, "#00ff00" }, { CustomRoles.Farmer, "#FFDE59" }, { CustomRoles.Transmitter, "#c9a11e" }, + { CustomRoles.Captain, "#53B3EF" }, { CustomRoles.Tree, "#00ff00" }, { CustomRoles.Inquisitor, "#7726B6" }, { CustomRoles.Imitator, "#c99e28" }, diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 9154b9476..f49185ed6 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -235,6 +235,7 @@ internal static class CustomRolesHelper CustomRoles.MeetingManager => CustomRoles.Crewmate, CustomRoles.Bane => CustomRoles.Crewmate, CustomRoles.Farmer => CustomRoles.Crewmate, + CustomRoles.Captain => CustomRoles.Crewmate, CustomRoles.Transmitter => CustomRoles.Crewmate, CustomRoles.Tree => CustomRoles.Crewmate, CustomRoles.Inquisitor => CustomRoles.Crewmate, @@ -839,6 +840,7 @@ internal static class CustomRolesHelper public static bool UsesMeetingShapeshift(this CustomRoles role) { + if (!Options.UseMeetingShapeshift.GetBool()) return false; Type type = role.GetRoleClass().GetType(); return type.GetMethod("OnMeetingShapeshift")?.DeclaringType == type; } @@ -1745,6 +1747,7 @@ internal static class CustomRolesHelper CustomRoles.Tracefinder => RoleOptionType.Crewmate_Miscellaneous, CustomRoles.Tunneler => RoleOptionType.Crewmate_Miscellaneous, CustomRoles.Analyst => RoleOptionType.Crewmate_Investigate, + CustomRoles.Captain => RoleOptionType.Crewmate_Investigate, CustomRoles.Catcher => RoleOptionType.Crewmate_Investigate, CustomRoles.Chameleon => RoleOptionType.Crewmate_Investigate, CustomRoles.Clairvoyant => RoleOptionType.Crewmate_Investigate, diff --git a/Modules/CustomSabotage.cs b/Modules/CustomSabotage.cs index da9ee58f7..e3af8299e 100644 --- a/Modules/CustomSabotage.cs +++ b/Modules/CustomSabotage.cs @@ -162,7 +162,7 @@ public class GrabOxygenMaskSabotage : CustomSabotage if (LastReactorFlash + 2 <= now) { LastReactorFlash = now; - aapc.ExceptBy(HasMask, x => x.PlayerId).Do(x => x.ReactorFlash(flashDuration: 0.1f)); + aapc.ExceptBy(HasMask, x => x.PlayerId).Do(x => x.ReactorFlash(flashDuration: 0.5f)); } if (LastNotify != now) diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 67ebfc15c..072126064 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -9262,6 +9262,11 @@ "Listener.FullMessageHearChance": "Full Message Hear Chance", "Listener.FullMessage": "{0}\\n{1} whispers to {2}:\\n{3}", "Listener.FromTo": "{0}\\n{1} whispers to {2}", + "Captain": "Captain", + "CaptainInfo": "Track Players' Movements", + "CaptainInfoLong": "(Crewmates):\\nAs the Captain, you always see who is in the same room as you. Additionally, your vote target will be tracked, and you'll get a list of rooms they've entered (in order) in the next round in the next meeting.", + "Captain.TargetInfo": "{0} entered the following rooms this round, in this order:\\n{1}", + "Captain.Suffix": "Players in the same room as you:\\n{0}", "Ambusher": "Ambusher", "AmbusherInfo": "Ambush Players to Make Them Fragile", "AmbusherInfoLong": "(Impostors):\\nAs the Ambusher, use your ability to become invisible. Follow someone for a few seconds to ambush them. If you succeed, the player gets Fragile effects. Otherwise, you become visible once your invisibility duration runs out and nothing happens.", diff --git a/Roles/Crewmate/Captain.cs b/Roles/Crewmate/Captain.cs new file mode 100644 index 000000000..8fea21cc9 --- /dev/null +++ b/Roles/Crewmate/Captain.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Linq; +using EHR.Modules; +using EHR.Neutral; +using Hazel; + +namespace EHR.Crewmate; + +public class Captain : RoleBase +{ + public static bool On; + + public override bool IsEnable => On; + + private static OptionItem CancelVote; + + private string LastNotify; + private byte CaptainId; + private int Count; + private byte TargetId; + private List TargetRooms; + + public override void SetupCustomOption() + { + StartSetup(655300) + .CreateVoteCancellingUseSetting(ref CancelVote); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + LastNotify = string.Empty; + CaptainId = playerId; + Count = 0; + TargetId = byte.MaxValue; + TargetRooms = []; + } + + 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; + + TargetId = target.PlayerId; + + Main.DontCancelVoteList.Add(voter.PlayerId); + return true; + } + + public override void OnMeetingShapeshift(PlayerControl shapeshifter, PlayerControl target) + { + OnVote(shapeshifter, target); + } + + public override void OnGlobalFixedUpdate(PlayerControl pc, bool lowLoad) + { + if (lowLoad || pc.PlayerId != TargetId || !GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks) return; + + var room = pc.GetPlainShipRoom(); + + if (room != null && (TargetRooms.Count == 0 || TargetRooms[^1] != room.RoomId)) + TargetRooms.Add(room.RoomId); + } + + public override void OnReportDeadBody() + { + if (TargetRooms.Count == 0) return; + + LateTask.New(() => + { + string msg = string.Format(Translator.GetString("Captain.TargetInfo"), TargetId.ColoredPlayerName(), string.Join(" ➔ ", TargetRooms.ConvertAll(x => Translator.GetString(x.ToString())))); + Utils.SendMessage(msg, CaptainId, CustomRoles.Captain.ToColoredString()); + TargetRooms = []; + }, 10f, "Captain Message"); + } + + public override void OnFixedUpdate(PlayerControl pc) + { + if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || Count++ < 20) return; + Count = 0; + + var room = pc.GetPlainShipRoom(); + + if (room == null) + { + if (string.IsNullOrEmpty(LastNotify)) return; + LastNotify = string.Empty; + Utils.SendRPC(CustomRPC.SyncRoleData, CaptainId, LastNotify); + Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); + return; + } + + string notify = string.Join(", ", Main.AllAlivePlayerControls.Without(pc).Where(x => x.GetPlainShipRoom() == room).Select(x => x.PlayerId.ColoredPlayerName())); + if (notify == LastNotify) return; + LastNotify = notify; + Utils.SendRPC(CustomRPC.SyncRoleData, CaptainId, LastNotify); + Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); + } + + public void ReceiveRPC(MessageReader reader) + { + LastNotify = reader.ReadString(); + } + + public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) + { + if (seer.PlayerId != CaptainId || seer.PlayerId != target.PlayerId || (seer.IsModdedClient() && !hud) || meeting) return string.Empty; + return string.Format(Translator.GetString("Captain.Suffix"), LastNotify); + } +} \ No newline at end of file diff --git a/Roles/Impostor/Psychopath.cs b/Roles/Impostor/Psychopath.cs index 22322092f..26d996561 100644 --- a/Roles/Impostor/Psychopath.cs +++ b/Roles/Impostor/Psychopath.cs @@ -33,7 +33,7 @@ public class Psychopath : RoleBase public override void OnFixedUpdate(PlayerControl pc) { - if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || Main.KillTimers[pc.PlayerId] > 0f || Count++ < 10) return; + if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || !pc.IsAlive() || Main.KillTimers[pc.PlayerId] > 0f || Count++ < 10) return; var pos = pc.Pos(); var killRange = NormalGameOptionsV10.KillDistances[Mathf.Clamp(Main.NormalOptions.KillDistance, 0, 2)]; PlayerControl closestPlayer = Main.AllAlivePlayerControls.Without(pc).Select(x => (pc: x, distance: Vector2.Distance(x.Pos(), pos))).Where(x => x.distance <= killRange).MinBy(x => x.distance).pc; From 236561a42395c72ff86036acd35aeb155e2bdb94 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 18 Sep 2025 16:33:02 +0200 Subject: [PATCH 24/24] a --- Roles/Impostor/Psychopath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Roles/Impostor/Psychopath.cs b/Roles/Impostor/Psychopath.cs index 26d996561..111aa1a62 100644 --- a/Roles/Impostor/Psychopath.cs +++ b/Roles/Impostor/Psychopath.cs @@ -36,7 +36,7 @@ public class Psychopath : RoleBase if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || !pc.IsAlive() || Main.KillTimers[pc.PlayerId] > 0f || Count++ < 10) return; var pos = pc.Pos(); var killRange = NormalGameOptionsV10.KillDistances[Mathf.Clamp(Main.NormalOptions.KillDistance, 0, 2)]; - PlayerControl closestPlayer = Main.AllAlivePlayerControls.Without(pc).Select(x => (pc: x, distance: Vector2.Distance(x.Pos(), pos))).Where(x => x.distance <= killRange).MinBy(x => x.distance).pc; + PlayerControl closestPlayer = Main.AllAlivePlayerControls.Without(pc).Where(x => !x.IsImpostor()).Select(x => (pc: x, distance: Vector2.Distance(x.Pos(), pos))).Where(x => x.distance <= killRange).MinBy(x => x.distance).pc; if (closestPlayer == null) return; Count = 0; pc.RpcCheckAndMurder(closestPlayer);