From 30c78537e846e2f0eeb3d5af2cf1d2084f146777 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Sun, 23 Jun 2024 13:53:07 +0200 Subject: [PATCH] speedrun game mode --- CustomRoles.cs | 3 + GameMode/SpeedrunManager.cs | 133 ++++++++++++++++++ Modules/CustomRoleSelector.cs | 3 + Modules/CustomRolesHelper.cs | 9 ++ Modules/ExtendedPlayerControl.cs | 7 +- .../PlayerGameOptionsSender.cs | 1 + Modules/GameState.cs | 6 + Modules/NameColorManager.cs | 3 + Modules/OptionHolder.cs | 19 ++- Modules/Utils.cs | 19 +-- Patches/CheckGameEndPatch.cs | 15 ++ Patches/GameOptionsMenuPatch.cs | 1 + Patches/HudPatch.cs | 7 + Patches/IntroPatch.cs | 19 +++ Patches/OnGameStartedPatch.cs | 6 +- Patches/OutroPatch.cs | 17 ++- Patches/PlayerControlPatch.cs | 11 ++ Resources/Lang/en_US.json | 14 +- Roles/Crewmate/Divinator.cs | 2 +- Roles/Crewmate/Sheriff.cs | 11 +- Roles/Impostor/Mafioso.cs | 13 +- main.cs | 2 + 22 files changed, 289 insertions(+), 32 deletions(-) create mode 100644 GameMode/SpeedrunManager.cs diff --git a/CustomRoles.cs b/CustomRoles.cs index 605e4f444..20e658a74 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -342,6 +342,9 @@ public enum CustomRoles Agent, Taskinator, + // Speedrun + Runner, + // GM GM, diff --git a/GameMode/SpeedrunManager.cs b/GameMode/SpeedrunManager.cs new file mode 100644 index 000000000..81098b403 --- /dev/null +++ b/GameMode/SpeedrunManager.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; +using EHR.Modules; +using HarmonyLib; +using UnityEngine; + +namespace EHR +{ + public class SpeedrunManager + { + private static OptionItem TaskFinishWins; + private static OptionItem TimeStacksUp; + private static OptionItem TimeLimit; + + public static HashSet CanKill = []; + + private static Dictionary Timers = []; + + public static void SetupCustomOption() + { + const int id = 69_214_001; + Color color = Utils.GetRoleColor(CustomRoles.Speedrunner); + + TaskFinishWins = new BooleanOptionItem(id, "Speedrun_TaskFinishWins", false, TabGroup.GameSettings) + .SetGameMode(CustomGameMode.Speedrun) + .SetColor(color); + + TimeStacksUp = new BooleanOptionItem(id + 1, "Speedrun_TimeStacksUp", false, TabGroup.GameSettings) + .SetGameMode(CustomGameMode.Speedrun) + .SetColor(color); + + TimeLimit = new IntegerOptionItem(id + 2, "Speedrun_TimeLimit", new(1, 90, 1), 20, TabGroup.GameSettings) + .SetGameMode(CustomGameMode.Speedrun) + .SetValueFormat(OptionFormat.Seconds) + .SetColor(color); + } + + public static void Init() + { + CanKill = []; + Timers = Main.AllAlivePlayerControls.ToDictionary(x => x.PlayerId, _ => TimeLimit.GetInt()); + } + + public static void ResetTimer(PlayerControl pc) + { + if (TimeStacksUp.GetBool()) Timers[pc.PlayerId] += TimeLimit.GetInt(); + else Timers[pc.PlayerId] = TimeLimit.GetInt(); + } + + public static void OnTaskFinish(PlayerControl pc) + { + if (TaskFinishWins.GetBool()) return; + + CanKill.Add(pc.PlayerId); + pc.ChangeRoleBasis(RoleTypes.Impostor); + pc.Notify(Translator.GetString("Speedrun_CompletedTasks")); + } + + public static string GetTaskBarText() + { + return string.Join('\n', Main.PlayerStates + .Join(Main.AllAlivePlayerControls, x => x.Key, x => x.PlayerId, (kvp, pc) => ( + Name: Utils.ColorString(Main.PlayerColors.GetValueOrDefault(kvp.Key, Color.white), pc.GetRealName()), + CompletedTasks: kvp.Value.TaskState.CompletedTasksCount, + AllTasks: kvp.Value.TaskState.AllTasksCount)) + .Select(x => $"{x.Name}: {x.CompletedTasks}/{x.AllTasks}")); + } + + public static string GetSuffixText(PlayerControl pc) + { + int time = Timers[pc.PlayerId]; + int alive = Main.AllAlivePlayerControls.Length; + int apc = Main.AllPlayerControls.Length; + int killers = CanKill.Count; + + // ReSharper disable once ConvertIfStatementToReturnStatement + if (CanKill.Contains(pc.PlayerId)) return string.Format(Translator.GetString("Speedrun_CanKillSuffixInfo"), alive, apc, killers - 1, time); + return string.Format(Translator.GetString("Speedrun_DoTasksSuffixInfo"), pc.GetTaskState().RemainingTasksCount, alive, apc, killers, time); + } + + public static bool CheckForGameEnd(out GameOverReason reason) + { + if (TaskFinishWins.GetBool()) + { + var player = Main.AllAlivePlayerControls.FirstOrDefault(x => x.GetTaskState().IsTaskFinished); + if (player != null) + { + CustomWinnerHolder.WinnerIds = [player.PlayerId]; + reason = GameOverReason.HumansByTask; + return true; + } + } + else + { + switch (Main.AllAlivePlayerControls.Length) + { + case 1: + CustomWinnerHolder.WinnerIds = [Main.AllAlivePlayerControls[0].PlayerId]; + reason = GameOverReason.ImpostorByKill; + return true; + case 0: + CustomWinnerHolder.WinnerIds = []; + reason = GameOverReason.HumansDisconnect; + return true; + } + } + + reason = GameOverReason.ImpostorByKill; + return false; + } + + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))] + class FixedUpdatePatch + { + private static long LastUpdate; + + public static void Postfix(PlayerControl __instance) + { + if (!AmongUsClient.Instance.AmHost || !GameStates.IsInTask || Options.CurrentGameMode != CustomGameMode.Speedrun || Main.HasJustStarted) return; + + if (Timers[__instance.PlayerId] <= 0) __instance.Suicide(); + + long now = Utils.TimeStamp; + if (LastUpdate == now) return; + LastUpdate = now; + + Timers.Keys.ToArray().Do(x => Timers[x]--); + Utils.NotifyRoles(); + } + } + } +} \ No newline at end of file diff --git a/Modules/CustomRoleSelector.cs b/Modules/CustomRoleSelector.cs index 6d36f89b2..80d96ae75 100644 --- a/Modules/CustomRoleSelector.cs +++ b/Modules/CustomRoleSelector.cs @@ -56,6 +56,9 @@ internal static class CustomRoleSelector case CustomGameMode.HotPotato: AssignRoleToEveryone(CustomRoles.Potato); return; + case CustomGameMode.Speedrun: + AssignRoleToEveryone(CustomRoles.Runner); + return; case CustomGameMode.HideAndSeek: HnSManager.AssignRoles(); RoleResult = HnSManager.PlayerRoles.ToDictionary(x => Utils.GetPlayerById(x.Key), x => x.Value.Role); diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index cf7b168d3..936daddf3 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -44,6 +44,13 @@ internal static class CustomRolesHelper public static bool IsExperimental(this CustomRoles role) => ExperimentalRoleList.Contains(role); + public static bool IsForOtherGameMode(this CustomRoles role) => HnSManager.AllHnSRoles.Contains(role) || role is + CustomRoles.KB_Normal or + CustomRoles.Killer or + CustomRoles.Tasker or + CustomRoles.Potato or + CustomRoles.Runner; + public static RoleBase GetRoleClass(this CustomRoles role) { var roleClass = role switch @@ -334,6 +341,8 @@ internal static class CustomRolesHelper CustomRoles.Tasker => RoleTypes.Crewmate, // Hot Potato CustomRoles.Potato => RoleTypes.Crewmate, + // Speedrun + CustomRoles.Runner => RoleTypes.Crewmate, // Standard CustomRoles.Executioner => Executioner.CRoleChangeRoles[Executioner.ChangeRolesAfterTargetKilled.GetValue()].GetDYRole(), CustomRoles.Sheriff => UsePets && Sheriff.UsePet.GetBool() ? RoleTypes.GuardianAngel : RoleTypes.Impostor, diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 3d650e317..6b96ddff4 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -487,7 +487,7 @@ static class ExtendedPlayerControl public static string GetNameWithRole(this PlayerControl player, bool forUser = false) { - return $"{player?.Data?.PlayerName}" + (GameStates.IsInGame && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato ? $" ({player?.GetAllRoleName(forUser).RemoveHtmlTags().Replace('\n', ' ')})" : string.Empty); + return $"{player?.Data?.PlayerName}" + (GameStates.IsInGame && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun ? $" ({player?.GetAllRoleName(forUser).RemoveHtmlTags().Replace('\n', ' ')})" : string.Empty); } public static string GetRoleColorCode(this PlayerControl player) @@ -588,6 +588,7 @@ static class ExtendedPlayerControl if (Mastermind.ManipulatedPlayers.ContainsKey(pc.PlayerId)) return true; if (Penguin.IsVictim(pc)) return false; if (Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.MoveAndStop) return false; + if (Options.CurrentGameMode == CustomGameMode.Speedrun && !SpeedrunManager.CanKill.Contains(pc.PlayerId)) return false; if (Pelican.IsEaten(pc.PlayerId)) return false; if (pc.Data.Role.Role == RoleTypes.GuardianAngel) return false; @@ -601,6 +602,8 @@ static class ExtendedPlayerControl CustomRoles.Tasker => false, // Hot Potato CustomRoles.Potato => false, + // Speedrun + CustomRoles.Runner => SpeedrunManager.CanKill.Contains(pc.PlayerId), // Hide And Seek CustomRoles.Seeker => true, CustomRoles.Hider => false, @@ -638,6 +641,8 @@ static class ExtendedPlayerControl CustomRoles.Tasker => false, // Hot Potato CustomRoles.Potato => false, + // Speedrun + CustomRoles.Runner => false, _ => Main.PlayerStates.TryGetValue(pc.PlayerId, out var state) && state.Role.CanUseImpostorVentButton(pc) }; diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index ac28e90a9..f3be0a612 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -159,6 +159,7 @@ public class PlayerGameOptionsSender(PlayerControl player) : GameOptionsSender } break; + case CustomGameMode.Speedrun: case CustomGameMode.HotPotato: case CustomGameMode.MoveAndStop: opt.SetVision(true); diff --git a/Modules/GameState.cs b/Modules/GameState.cs index 707e8bfd3..151881af5 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -317,6 +317,12 @@ public class TaskState { bool alive = player.IsAlive(); + if (alive && Options.CurrentGameMode == CustomGameMode.Speedrun) + { + if (CompletedTasksCount + 1 >= AllTasksCount) SpeedrunManager.OnTaskFinish(player); + else SpeedrunManager.ResetTimer(player); + } + if (player.Is(CustomRoles.Unlucky) && alive) { var Ue = IRandom.Instance; diff --git a/Modules/NameColorManager.cs b/Modules/NameColorManager.cs index 83c92eb2e..26d3ebc01 100644 --- a/Modules/NameColorManager.cs +++ b/Modules/NameColorManager.cs @@ -53,6 +53,9 @@ public static class NameColorManager return true; case CustomGameMode.HideAndSeek: return HnSManager.KnowTargetRoleColor(seer, target, ref color); + case CustomGameMode.Speedrun when SpeedrunManager.CanKill.Contains(target.PlayerId): + color = Main.ImpostorColor; + return true; } // Global (low priority) diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index 56ab3dae8..bd055b119 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -22,6 +22,7 @@ public enum CustomGameMode MoveAndStop = 0x04, HotPotato = 0x05, HideAndSeek = 0x06, + Speedrun = 0x07, All = int.MaxValue } @@ -42,7 +43,8 @@ public static class Options "FFA", "MoveAndStop", "HotPotato", - "HideAndSeek" + "HideAndSeek", + "Speedrun" ]; private static Dictionary roleCounts; @@ -762,6 +764,7 @@ public static class Options 3 => CustomGameMode.MoveAndStop, 4 => CustomGameMode.HotPotato, 5 => CustomGameMode.HideAndSeek, + 6 => CustomGameMode.Speedrun, _ => CustomGameMode.Standard }; @@ -787,7 +790,7 @@ public static class Options var sb = new System.Text.StringBuilder(); var grouped = Enum.GetValues().GroupBy(x => { - if (x is CustomRoles.GM or CustomRoles.Philantropist or CustomRoles.Konan or CustomRoles.KB_Normal or CustomRoles.Killer or CustomRoles.Potato or CustomRoles.Tasker or CustomRoles.NotAssigned or CustomRoles.LovingCrewmate or CustomRoles.LovingImpostor or CustomRoles.Convict || HnSManager.AllHnSRoles.Contains(x) || x.IsVanilla() || x.ToString().Contains("EHR")) return 4; + if (x is CustomRoles.GM or CustomRoles.Philantropist or CustomRoles.Konan or CustomRoles.NotAssigned or CustomRoles.LovingCrewmate or CustomRoles.LovingImpostor or CustomRoles.Convict || x.IsForOtherGameMode() || x.IsVanilla() || x.ToString().Contains("EHR")) return 4; if (x.IsAdditionRole()) return 3; if (x.IsImpostor() || x.IsMadmate()) return 0; if (x.IsNeutral()) return 1; @@ -1247,15 +1250,17 @@ public static class Options MainLoadingText = "Building Settings for Other Gamemodes"; - //SoloKombat + // SoloKombat SoloKombatManager.SetupCustomOption(); - //FFA + // FFA FFAManager.SetupCustomOption(); - //Move And Stop + // Move And Stop MoveAndStopManager.SetupCustomOption(); - //Hot Potato + // Hot Potato HotPotatoManager.SetupCustomOption(); - //Hide And Seek + // Speedrun + SpeedrunManager.SetupCustomOption(); + // Hide And Seek HnSManager.SetupCustomOption(); diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 6ef181fef..77c3ac150 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -625,6 +625,7 @@ public static class Utils case CustomGameMode.FFA: return false; case CustomGameMode.MoveAndStop: return true; case CustomGameMode.HotPotato: return false; + case CustomGameMode.Speedrun: return true; case CustomGameMode.HideAndSeek: return HnSManager.HasTasks(p); } @@ -808,7 +809,7 @@ public static class Utils public static bool IsRoleTextEnabled(PlayerControl __instance) { - if (__instance.AmOwner || Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.SoloKombat or CustomGameMode.MoveAndStop or CustomGameMode.HotPotato || (Options.CurrentGameMode == CustomGameMode.HideAndSeek && HnSManager.IsRoleTextEnabled(PlayerControl.LocalPlayer, __instance)) || Main.VisibleTasksCount && PlayerControl.LocalPlayer.Data.IsDead && Options.GhostCanSeeOtherRoles.GetBool() || PlayerControl.LocalPlayer.Is(CustomRoles.Mimic) && Main.VisibleTasksCount && __instance.Data.IsDead && Options.MimicCanSeeDeadRoles.GetBool()) return true; + if (__instance.AmOwner || Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.SoloKombat or CustomGameMode.MoveAndStop or CustomGameMode.HotPotato or CustomGameMode.Speedrun || (Options.CurrentGameMode == CustomGameMode.HideAndSeek && HnSManager.IsRoleTextEnabled(PlayerControl.LocalPlayer, __instance)) || Main.VisibleTasksCount && PlayerControl.LocalPlayer.Data.IsDead && Options.GhostCanSeeOtherRoles.GetBool() || PlayerControl.LocalPlayer.Is(CustomRoles.Mimic) && Main.VisibleTasksCount && __instance.Data.IsDead && Options.MimicCanSeeDeadRoles.GetBool()) return true; switch (__instance.GetCustomRole()) { @@ -1276,13 +1277,8 @@ public static class Utils } break; + case CustomGameMode.Speedrun: case CustomGameMode.HotPotato: - foreach (byte id in cloneRoles) - { - sb.Append("\n\u3000 ").Append(EndGamePatch.SummaryText[id]); - } - - break; case CustomGameMode.HideAndSeek: foreach (byte id in cloneRoles) { @@ -1793,6 +1789,7 @@ public static class Utils CustomGameMode.MoveAndStop => $"{modeText}\r\n" + name, CustomGameMode.HotPotato => $"{modeText}\r\n" + name, CustomGameMode.HideAndSeek => $"{modeText}\r\n" + name, + CustomGameMode.Speedrun => ColorString(GetRoleColor(CustomRoles.Speedrunner), $"{modeText}\r\n") + name, _ => name }; } @@ -2035,6 +2032,9 @@ public static class Utils case CustomGameMode.HotPotato when seer.IsAlive() && !seer.IsModClient(): SelfSuffix.Append(HotPotatoManager.GetSuffixText(seer.PlayerId)); break; + case CustomGameMode.Speedrun: + SelfSuffix.Append(SpeedrunManager.GetSuffixText(seer)); + break; case CustomGameMode.HideAndSeek: SelfSuffix.Append(HnSManager.GetSuffixText(seer, seer)); break; @@ -2048,7 +2048,7 @@ public static class Utils if (Options.CurrentGameMode == CustomGameMode.FFA && FFAManager.FFATeamMode.GetBool()) SeerRealName = SeerRealName.ApplyNameColorData(seer, seer, isForMeeting); - if (!isForMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool() && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato) + if (!isForMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool() && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun) { var team = CustomTeamManager.GetCustomTeam(seer.PlayerId); if (team != null) @@ -2254,7 +2254,7 @@ public static class Utils CustomTeamManager.AreInSameCustomTeam(seer.PlayerId, target.PlayerId) && CustomTeamManager.IsSettingEnabledForPlayerTeam(seer.PlayerId, CTAOption.KnowRoles) || Main.PlayerStates.Values.Any(x => x.Role.KnowRole(seer, target)) || Markseeker.PlayerIdList.Any(x => Main.PlayerStates[x].Role is Markseeker { IsEnable: true, TargetRevealed: true } ms && ms.MarkedId == target.PlayerId) || - Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.MoveAndStop or CustomGameMode.HotPotato || + Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.MoveAndStop or CustomGameMode.HotPotato or CustomGameMode.Speedrun || (Options.CurrentGameMode == CustomGameMode.HideAndSeek && HnSManager.IsRoleTextEnabled(seer, target)) || (seer.IsRevealedPlayer(target) && !target.Is(CustomRoles.Trickster)) || seer.Is(CustomRoles.God) || @@ -2860,6 +2860,7 @@ public static class Utils case CustomGameMode.FFA: summary = $"{ColorString(Main.PlayerColors[id], name)} {GetKillCountText(id, ffa: true)}"; break; + case CustomGameMode.Speedrun: case CustomGameMode.MoveAndStop: summary = $"{ColorString(Main.PlayerColors[id], name)} -{TaskCount.Replace("(", string.Empty).Replace(")", string.Empty)} ({GetVitalText(id, true)})"; break; diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index ead82fc0f..6ea8dca12 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -357,6 +357,7 @@ class GameEndChecker public static void SetPredicateToFFA() => Predicate = new FFAGameEndPredicate(); public static void SetPredicateToMoveAndStop() => Predicate = new MoveAndStopGameEndPredicate(); public static void SetPredicateToHotPotato() => Predicate = new HotPotatoGameEndPredicate(); + public static void SetPredicateToSpeedrun() => Predicate = new SpeedrunGameEndPredicate(); public static void SetPredicateToHideAndSeek() => Predicate = new HideAndSeekGameEndPredicate(); class NormalGameEndPredicate : GameEndPredicate @@ -686,6 +687,20 @@ class GameEndChecker } } + class SpeedrunGameEndPredicate : GameEndPredicate + { + public override bool CheckForEndGame(out GameOverReason reason) + { + reason = GameOverReason.ImpostorByKill; + return WinnerIds.Count <= 0 && CheckGameEndByLivingPlayers(out reason); + } + + private static bool CheckGameEndByLivingPlayers(out GameOverReason reason) + { + return SpeedrunManager.CheckForGameEnd(out reason); + } + } + public abstract class GameEndPredicate { /// Checks the game ending condition and stores the value in CustomWinnerHolder. diff --git a/Patches/GameOptionsMenuPatch.cs b/Patches/GameOptionsMenuPatch.cs index 802330f3c..5c93b54ee 100644 --- a/Patches/GameOptionsMenuPatch.cs +++ b/Patches/GameOptionsMenuPatch.cs @@ -510,6 +510,7 @@ public static class SetRecommendationsPatch __instance.KillCooldown = 0f; __instance.NumEmergencyMeetings = 0; break; + case CustomGameMode.Speedrun: case CustomGameMode.MoveAndStop: __instance.CrewLightMod = 1.25f; __instance.ImpostorLightMod = 1.25f; diff --git a/Patches/HudPatch.cs b/Patches/HudPatch.cs index 4ce5e209b..c24018bb2 100644 --- a/Patches/HudPatch.cs +++ b/Patches/HudPatch.cs @@ -407,6 +407,7 @@ class SetHudActivePatch { case CustomGameMode.MoveAndStop: case CustomGameMode.HotPotato: + case CustomGameMode.Speedrun: __instance.ReportButton?.ToggleVisible(false); __instance.KillButton?.ToggleVisible(false); __instance.SabotageButton?.ToggleVisible(false); @@ -698,6 +699,12 @@ class TaskPanelBehaviourPatch AllText += $"\r\n\r\n{HnSManager.GetTaskBarText()}"; + break; + + case CustomGameMode.Speedrun: + + AllText += $"\r\n\r\n{SpeedrunManager.GetTaskBarText()}"; + break; } diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index 95b28a07f..97bdceb80 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -68,6 +68,16 @@ class SetUpRoleTextPatch __instance.RoleBlurbText.text = GetString("PotatoInfo"); break; } + case CustomGameMode.Speedrun: + { + var color = Utils.GetRoleColor(CustomRoles.Speedrunner); + __instance.YouAreText.transform.gameObject.SetActive(false); + __instance.RoleText.text = GetString("Runner"); + __instance.RoleText.color = color; + __instance.RoleBlurbText.color = color; + __instance.RoleBlurbText.text = GetString("RunnerInfo"); + break; + } default: { CustomRoles role = PlayerControl.LocalPlayer.GetCustomRole(); @@ -508,6 +518,15 @@ class BeginCrewmatePatch __instance.ImpostorText.text = GetString("PotatoInfo"); break; } + case CustomGameMode.Speedrun: + { + __instance.TeamTitle.text = GetString("Runner"); + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = Utils.GetRoleColor(CustomRoles.Speedrunner); + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Crewmate); + __instance.ImpostorText.gameObject.SetActive(true); + __instance.ImpostorText.text = GetString("RunnerInfo"); + break; + } case CustomGameMode.HideAndSeek: { __instance.TeamTitle.text = GetString("HideAndSeek"); diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index a2b61319d..25cf09b7b 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -317,6 +317,7 @@ internal class ChangeRoleSettings MoveAndStopManager.Init(); HotPotatoManager.Init(); HnSManager.Init(); + SpeedrunManager.Init(); } catch (Exception e) { @@ -757,6 +758,9 @@ internal class SelectRolesPatch case CustomGameMode.HotPotato: GameEndChecker.SetPredicateToHotPotato(); break; + case CustomGameMode.Speedrun: + GameEndChecker.SetPredicateToSpeedrun(); + break; case CustomGameMode.HideAndSeek: GameEndChecker.SetPredicateToHideAndSeek(); break; @@ -769,7 +773,7 @@ internal class SelectRolesPatch } // Add players with unclassified roles to the list of players who require ResetCam. - Main.ResetCamPlayerList.AddRange(Main.AllPlayerControls.Where(p => p.GetCustomRole() is CustomRoles.Arsonist or CustomRoles.Revolutionist or CustomRoles.Sidekick or CustomRoles.KB_Normal or CustomRoles.Killer or CustomRoles.Tasker or CustomRoles.Potato or CustomRoles.Seeker or CustomRoles.Hider or CustomRoles.Fox or CustomRoles.Troll or CustomRoles.Jumper or CustomRoles.Detector or CustomRoles.Jet or CustomRoles.Dasher or CustomRoles.Locator or CustomRoles.Venter or CustomRoles.Agent or CustomRoles.Taskinator or CustomRoles.Innocent || (p.Is(CustomRoles.Witness) && (!Options.UsePets.GetBool() || Options.WitnessUsePet.GetBool()))).Select(p => p.PlayerId)); + Main.ResetCamPlayerList.AddRange(Main.AllPlayerControls.Where(p => p.GetCustomRole() is CustomRoles.Arsonist or CustomRoles.Revolutionist or CustomRoles.Sidekick or CustomRoles.Innocent || p.GetCustomRole().IsForOtherGameMode() || (p.Is(CustomRoles.Witness) && (!Options.UsePets.GetBool() || Options.WitnessUsePet.GetBool()))).Select(p => p.PlayerId)); Utils.CountAlivePlayers(true); Utils.SyncAllSettings(); diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index d4c0970bb..bc8a0f9cd 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -63,7 +63,7 @@ class EndGamePatch if (date == DateTime.MinValue) continue; var killerId = value.GetRealKiller(); var gmIsFM = Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.MoveAndStop; - var gmIsFMHH = gmIsFM || Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.HideAndSeek; + var gmIsFMHH = gmIsFM || Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.HideAndSeek or CustomGameMode.Speedrun; sb.Append($"\n{date:T} {Main.AllPlayerNames[key]} ({(gmIsFMHH ? string.Empty : Utils.GetDisplayRoleName(key, true))}{(gmIsFM ? string.Empty : Utils.GetSubRolesText(key, summary: true))}) [{Utils.GetVitalText(key)}]"); if (killerId != byte.MaxValue && killerId != key) sb.Append($"\n\t⇐ {Main.AllPlayerNames[killerId]} ({(gmIsFMHH ? string.Empty : Utils.GetDisplayRoleName(killerId, true))}{(gmIsFM ? string.Empty : Utils.GetSubRolesText(killerId, summary: true))})"); @@ -250,6 +250,14 @@ class SetEverythingUpPatch WinnerText.color = Main.PlayerColors[winnerId]; goto EndOfText; } + case CustomGameMode.Speedrun: + { + var winnerId = CustomWinnerHolder.WinnerIds.FirstOrDefault(); + __instance.BackgroundBar.material.color = Utils.GetRoleColor(CustomRoles.Speedrunner); + WinnerText.text = Main.AllPlayerNames[winnerId] + " wins!"; + WinnerText.color = Main.PlayerColors[winnerId]; + goto EndOfText; + } } if (CustomWinnerHolder.WinnerTeam == CustomWinner.CustomTeam) @@ -408,6 +416,13 @@ class SetEverythingUpPatch sb.Append("\n\u3000 ").Append(EndGamePatch.SummaryText[id]); break; } + case CustomGameMode.Speedrun: + { + var list = cloneRoles.OrderByDescending(id => Main.PlayerStates[id].TaskState.CompletedTasksCount); + foreach (var id in list.Where(EndGamePatch.SummaryText.ContainsKey)) + sb.Append("\n\u3000 ").Append(EndGamePatch.SummaryText[id]); + break; + } default: { sb.Append("\n"); diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 40ba3bf98..1560686fa 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -219,6 +219,11 @@ class CheckMurderPatch case CustomGameMode.MoveAndStop: case CustomGameMode.HotPotato: return false; + case CustomGameMode.Speedrun when !SpeedrunManager.CanKill.Contains(killer.PlayerId): + return false; + case CustomGameMode.Speedrun: + killer.Kill(target); + return false; case CustomGameMode.HideAndSeek: HnSManager.OnCheckMurder(killer, target); return false; @@ -589,6 +594,8 @@ class MurderPlayerPatch } Main.PlayerStates[killer.PlayerId].Role.OnMurder(killer, target); + + if (Options.CurrentGameMode == CustomGameMode.Speedrun) SpeedrunManager.ResetTimer(killer); if (killer.Is(CustomRoles.TicketsStealer) && killer.PlayerId != target.PlayerId) killer.Notify(string.Format(GetString("TicketsStealerGetTicket"), ((Main.AllPlayerControls.Count(x => x.GetRealKiller()?.PlayerId == killer.PlayerId) + 1) * Options.TicketsPerKill.GetFloat()).ToString("0.0#####"))); @@ -1561,6 +1568,9 @@ class FixedUpdatePatch case CustomGameMode.HotPotato when !seer.IsModClient() && self && seer.IsAlive(): Suffix.Append(HotPotatoManager.GetSuffixText(seer.PlayerId)); break; + case CustomGameMode.Speedrun when self: + Suffix.Append(SpeedrunManager.GetSuffixText(seer)); + break; case CustomGameMode.HideAndSeek: Suffix.Append(HnSManager.GetSuffixText(seer, target)); break; @@ -1792,6 +1802,7 @@ class CoEnterVentPatch return true; case CustomGameMode.MoveAndStop: case CustomGameMode.HotPotato: + case CustomGameMode.Speedrun: LateTask.New(() => { __instance.myPlayer?.MyPhysics?.RpcBootFromVent(id); }, 0.5f, log: false); return true; case CustomGameMode.HideAndSeek: diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index e15abff46..684eac6c4 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -3554,9 +3554,21 @@ "Potato": "Player", "PotatoInfo": "AVOID HAVING THE HOT POTATO", "PotatoInfoLong": "AVOID HAVING THE HOT POTATO", + "Speedrun": "SPEEDRUN", + "ModeSpeedrun": "Gamemode: Speedrun", + "ModeDescribe.Speedrun": "In the Speedrun gamemode, everything is about speed. Everyone has to complete all tasks as fast as possible, then kill everyone else as fast as possible!\\n\\nWhen someone finishes all their tasks, they become a killer and can kill everyone else. The last player alive wins!\\nBut to make this a bit harder, you have a time limit to complete a task or kill someone! The timer resets when you do so. If your time runs out, you die!\\n\\nThis gamemode is a hybrid of the Stop And Go and Free For All gamemodes.", + "Speedrun_TaskFinishWins": "First to Finish Tasks Wins Immediately (skips killing phase)", + "Speedrun_TimeStacksUp": "Time Remainder Adds to Next Time Limit (time stacks up)", + "Speedrun_TimeLimit": "Time Limit to Do a Task or Kill", + "Speedrun_CanKillSuffixInfo": "<#00ffa5>Time: {3}s\\n<#ffff00>Alive players: <#ffab1b>{0}/{1}\\n<#ff1919>Kill everyone to win\\n<#888888>Apart from you, <#ffffff>{2} players can kill", + "Speedrun_DoTasksSuffixInfo": "<#00ffa5>Time: {4}s\\n<#888888>Alive players: <#ffffff>{1}/{2}\\n<#ffff00>Remaining tasks: {0}\\n<#00ffa5>Finish your tasks\\n<#ffab1b><#ffffff>{3} players can kill", + "Speedrun_CompletedTasks": "<#00ff00>You're done with tasks!", + "Runner": "Speedrunner", + "RunnerInfo": "COMPLETE TASKS AND KILL EVERYONE FIRST TO WIN", + "RunnerInfoLong": "COMPLETE TASKS AND KILL EVERYONE FIRST TO WIN", "HideAndSeek": "HIDE AND SEEK", "ModeHideAndSeek": "Gamemode: Hide And Seek", - "ModeDescribe.HideAndSeek": "In the Hide And Seek gamemode, seekers have to kill all hiders, while hiders have to complete all tasks without being killed in order to win.\\nIf the time limit reaches 0, hiders win.\\n\\nRoles:\\n\\n<#00ff00>Fox:\\nWins as long as they're alive at the end.\\n\\n<#ff00ff>Troll:\\nWins if they die. Seekers and hiders lose.", + "ModeDescribe.HideAndSeek": "In the Hide And Seek gamemode, seekers have to kill all hiders, while hiders have to complete all tasks without being killed in order to win.\\nIf the time limit reaches 0, hiders win.\\n\\nRoles: Hider, Seeker, Fox, Troll, Jumper, Detector, Jet, Dasher, Locator, Venter, Agent, Taskinator", "HideAndSeek_MaxGameTime": "Maximum Game Length", "HNS.MinNeutrals": "<#00ffff>Minimum Number of <#ffab1b>Neutrals", "HNS.MaxNeutrals": "<#0000ff>Maximum Number of <#ffab1b>Neutrals", diff --git a/Roles/Crewmate/Divinator.cs b/Roles/Crewmate/Divinator.cs index fdc08a907..0c5e9d146 100644 --- a/Roles/Crewmate/Divinator.cs +++ b/Roles/Crewmate/Divinator.cs @@ -62,7 +62,7 @@ public class Divinator : RoleBase var players = Main.AllAlivePlayerControls; int rolesNeeded = players.Length * (RolesPerCategory - 1); AllPlayerRoleList = Enum.GetValues() - .Where(x => !x.IsVanilla() && !x.IsAdditionRole() && x is not CustomRoles.Killer and not CustomRoles.Tasker and not CustomRoles.KB_Normal and not CustomRoles.Potato and not CustomRoles.GM and not CustomRoles.Convict && !HnSManager.AllHnSRoles.Contains(x)) + .Where(x => !x.IsVanilla() && !x.IsAdditionRole() && x is not CustomRoles.GM and not CustomRoles.Convict && !x.IsForOtherGameMode()) .OrderBy(x => x.IsEnable() ? IRandom.Instance.Next(10) : IRandom.Instance.Next(10, 100)) .Take(rolesNeeded) .Chunk(RolesPerCategory - 1) diff --git a/Roles/Crewmate/Sheriff.cs b/Roles/Crewmate/Sheriff.cs index 7f6a71b97..7d6c54626 100644 --- a/Roles/Crewmate/Sheriff.cs +++ b/Roles/Crewmate/Sheriff.cs @@ -66,12 +66,15 @@ public class Sheriff : RoleBase UsePet = Options.CreatePetUseSetting(Id + 29, CustomRoles.Sheriff); } - public static void SetUpNeutralOptions(int id) + private static void SetUpNeutralOptions(int id) { - foreach (var neutral in Enum.GetValues().Where(x => x.IsNeutral() && x is not CustomRoles.KB_Normal and not CustomRoles.Konan and not CustomRoles.Pestilence and not CustomRoles.Killer and not CustomRoles.Tasker and not CustomRoles.Potato and not CustomRoles.Hider and not CustomRoles.Seeker and not CustomRoles.Fox and not CustomRoles.Troll and not CustomRoles.Jumper and not CustomRoles.Detector and not CustomRoles.Jet and not CustomRoles.Dasher and not CustomRoles.Locator and not CustomRoles.Venter and not CustomRoles.Agent and not CustomRoles.Taskinator and not CustomRoles.GM and not CustomRoles.Convict)) + foreach (var neutral in Enum.GetValues()) { - SetUpKillTargetOption(neutral, id, true, CanKillNeutralsMode); - id++; + if (neutral.IsNeutral() && neutral is not CustomRoles.Konan and not CustomRoles.Pestilence and not CustomRoles.GM and not CustomRoles.Convict && !neutral.IsForOtherGameMode()) + { + SetUpKillTargetOption(neutral, id, true, CanKillNeutralsMode); + id++; + } } } diff --git a/Roles/Impostor/Mafioso.cs b/Roles/Impostor/Mafioso.cs index bf8ce0f09..8fb8f7edc 100644 --- a/Roles/Impostor/Mafioso.cs +++ b/Roles/Impostor/Mafioso.cs @@ -28,7 +28,7 @@ namespace EHR.Roles.Impostor public class Mafioso : RoleBase { private const int Id = 642200; - private static List playerIdList = []; + private static List PlayerIdList = []; private static OptionItem Delay; private static OptionItem RewardForKilling; @@ -47,7 +47,7 @@ namespace EHR.Roles.Impostor private int Tier; private int XP; - public override bool IsEnable => playerIdList.Count > 0; + public override bool IsEnable => PlayerIdList.Count > 0; public static void SetupCustomOption() { @@ -67,7 +67,7 @@ namespace EHR.Roles.Impostor public override void Init() { - playerIdList = []; + PlayerIdList = []; PreviouslyUsedVents = []; Tier = 0; XP = 0; @@ -79,7 +79,7 @@ namespace EHR.Roles.Impostor public override void Add(byte playerId) { - playerIdList.Add(playerId); + PlayerIdList.Add(playerId); lastUpdate = TimeStamp + 8; MafiosoId = playerId; PreviouslyUsedVents = []; @@ -273,9 +273,8 @@ namespace EHR.Roles.Impostor public override bool OnSabotage(PlayerControl pc) { - if (Main.PlayerStates[pc.PlayerId].Role is not Mafioso { IsEnable: true } mo) return true; - mo.XP += RewardForSabotaging.GetInt(); - mo.SendRPC(); + XP += RewardForSabotaging.GetInt(); + SendRPC(); return true; } diff --git a/main.cs b/main.cs index 11acfc157..0cea76fe6 100644 --- a/main.cs +++ b/main.cs @@ -598,6 +598,8 @@ public class Main : BasePlugin { CustomRoles.Tasker, "#00ffa5" }, // Hot Potato { CustomRoles.Potato, "#e8cd46" }, + // Speedrun + { CustomRoles.Runner, "#800080"}, // Hide And Seek { CustomRoles.Seeker, "#ff1919" }, { CustomRoles.Hider, "#345eeb" },