diff --git a/CustomRoles.cs b/CustomRoles.cs index eb51d237a..dacd9d105 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -434,6 +434,9 @@ public enum CustomRoles // Quiz QuizPlayer, + // The Mind Game + TMGPlayer, + // GM GM, diff --git a/EHR.sln.DotSettings b/EHR.sln.DotSettings index b1c4d96af..b61975463 100644 --- a/EHR.sln.DotSettings +++ b/EHR.sln.DotSettings @@ -60,6 +60,7 @@ SK SS SSCD + TMG TMP EHR TP diff --git a/GameMode/AllInOneGameMode.cs b/GameMode/AllInOneGameMode.cs index 403399f31..64b743ba3 100644 --- a/GameMode/AllInOneGameMode.cs +++ b/GameMode/AllInOneGameMode.cs @@ -89,11 +89,14 @@ public static class AllInOneGameMode CustomGameMode.CaptureTheFlag, CustomGameMode.FFA, CustomGameMode.SoloKombat, + CustomGameMode.KingOfTheZones, CustomGameMode.RoomRush, CustomGameMode.Speedrun, CustomGameMode.NaturalDisasters, CustomGameMode.MoveAndStop, - CustomGameMode.HotPotato + CustomGameMode.HotPotato, + CustomGameMode.Quiz, + CustomGameMode.TheMindGame }.First(x => x.IsActiveOrIntegrated()); } diff --git a/GameMode/TheMindGame.cs b/GameMode/TheMindGame.cs new file mode 100644 index 000000000..df355df7e --- /dev/null +++ b/GameMode/TheMindGame.cs @@ -0,0 +1,583 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace EHR; + +public static class TheMindGame +{ + private static Dictionary Points = []; + private static Dictionary SuperPoints = []; + private static Dictionary DefaultColorIds = []; + private static Dictionary Groups = []; + private static Dictionary> GroupPlayers = []; // TODO: Reset ALL these at the start of the game + private static List AllRooms = []; + private static Dictionary GroupRooms = []; + private static Dictionary Pick = []; + private static HashSet AmReady = []; + private static Dictionary> ItemIds = []; + private static Dictionary> PlayerItems = []; + private static long ProceedingInCountdownEndTS; + private static long LastTimeWarning; + private static bool ShowSuffixOtherThanPoints; + private static int AuctionValue; + private static byte WinningBriefcaseHolderId; + private static List Round4PlacesFromFirst; + private static List Round4PlacesFromLast; + private static List EjectedPlayers; + private static int Round = 1; + + // Settings + private static bool PlayersCanSeeOthersPoints = true; + private static int NumGroupsForRound1 = 5; // 1-7 + private static int TimeForEachPickInRound1 = 15; + private static int NumPointsToAdvanceInRound1 = 10; + private static int MinPlayersInRound2 = 10; + private static int SuperPointsToNormalPointsMultiplier = 3; + private static int TimeForEachPickInRound2 = 25; + private static int TimeForItemPurchasingInRound2 = 40; + private static int FoolNumPointsLost = 5; + private static Dictionary ItemCosts = []; // !!!! + private static int TimeForEachPickInRound3 = 30; + private static int MaxPlayersForRound4 = 5; + + private static bool Stop => GameStates.IsMeeting || ExileController.Instance || !GameStates.InGame || GameStates.IsLobby || GameStates.IsEnded; + + public static void SetupCustomOption() { } + public static string GetStatistics(byte id) { } + public static int GetPoints(byte id) { } + public static string GetTaskBarText() { } + public static string GetSuffix(PlayerControl seer, PlayerControl target) { } + + public static IEnumerator OnGameStart() + { + yield return null; + + Round = 1; + + AllRooms = ShipStatus.Instance.AllRooms.Select(x => x.RoomId).Distinct().Shuffle(); + AllRooms.Remove(SystemTypes.Hallway); + AllRooms.Remove(SystemTypes.Outside); + AllRooms.Remove(SystemTypes.Ventilation); + AllRooms.RemoveAll(x => x.ToString().Contains("Decontamination")); + + PlayerControl[] aapc = Main.AllAlivePlayerControls; + Points = aapc.ToDictionary(x => x.PlayerId, _ => 0); + SuperPoints = aapc.ToDictionary(x => x.PlayerId, _ => 0); + DefaultColorIds = aapc.ToDictionary(x => x, x => x.Data.DefaultOutfit.ColorId); + + { + IEnumerable> groups = aapc.Partition(NumGroupsForRound1); + RandomSpawn.SpawnMap map = RandomSpawn.SpawnMap.GetSpawnMap(); + GroupRooms = []; + Group group = default(Group); + + foreach (IEnumerable players in groups) + { + (SystemTypes room, Vector2 location) = map.Positions.ExceptBy(GroupRooms.Values, x => x.Key).RandomElement(); + GroupRooms[group] = room; + List ids = []; + + foreach (PlayerControl pc in players) + { + Groups[pc.PlayerId] = group; + pc.RpcSetColor(group.GetColorId()); + pc.TP(location); + ids.Add(pc.PlayerId); + } + + GroupPlayers[group] = ids; + group++; + } + } + + Main.Instance.StartCoroutine(PreventMovingOutOfGroupRooms()); + + Utils.SetChatVisibleForAll(); + + yield return NotifyEveryone("TMG.Tutorial.Basics", 4); + if (Stop) yield break; + + yield return NotifyEveryone("TMG.Notify.Round", 2, 1); + if (Stop) yield break; + + yield return NotifyEveryone("TMG.Tutorial.Round1", 10, TimeForEachPickInRound1, NumPointsToAdvanceInRound1, MinPlayersInRound2); + if (Stop) yield break; + + Main.AllAlivePlayerControls.Do(x => x.RpcChangeRoleBasis(CustomRoles.PhantomEHR)); + + while (true) + { + Pick = Main.AllAlivePlayerControls.ToDictionary(x => x.PlayerId, _ => IRandom.Instance.Next(1, 4)); + ProceedingInCountdownEndTS = Utils.TimeStamp + TimeForEachPickInRound1; + float timer = TimeForEachPickInRound1; + + ShowSuffixOtherThanPoints = true; + Utils.NotifyRoles(); + + while (timer > 0f) + { + timer -= Time.deltaTime; + if (Stop) yield break; + yield return null; + + if ((int)timer % 10 == 0 && LastTimeWarning + 2 < Utils.TimeStamp) + { + LastTimeWarning = Utils.TimeStamp; + Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.TimeLeft"), (int)timer), title: Translator.GetString("TMG.Message.TimeLeftTitle")); + } + } + + ShowSuffixOtherThanPoints = false; + + foreach (PlayerControl pc in Main.AllAlivePlayerControls) + { + Group group = Groups[pc.PlayerId]; + int pick = Pick[pc.PlayerId]; + bool sameAsSomeoneElse = GroupPlayers[group].FindFirst(x => x != pc.PlayerId && Pick[x] == pick, out byte otherId); + + if (!sameAsSomeoneElse) Points[pc.PlayerId] += pick; + + pc.Notify(string.Format(Translator.GetString(sameAsSomeoneElse ? "TMG.Notify.SamePick" : "TMG.Notify.Pick"), pick, sameAsSomeoneElse ? otherId.ColoredPlayerName() : string.Empty)); + } + + yield return new WaitForSeconds(3f); + if (Stop) yield break; + NameNotifyManager.Reset(); + + int playersAdvancing = Points.Count(x => x.Value >= NumPointsToAdvanceInRound1); + yield return NotifyEveryone("TMG.Notify.Round1NumPlayersAdvancing", 3, playersAdvancing, Main.AllAlivePlayerControls.Length, MinPlayersInRound2); + if (Stop) yield break; + + if (playersAdvancing >= MinPlayersInRound2) break; + } + + foreach (List playerIds in GroupPlayers.Values) + { + var points = playerIds.ToDictionary(x => x, x => Points[x]); + int lowestScore = points.Values.Min(); + playerIds.Intersect(Main.AllAlivePlayerControls.Select(x => x.PlayerId)).Join(points, x => x, x => x.Key, (id, kvp) => (id, points: kvp.Value)).DoIf(x => x.points == lowestScore, x => x.id.GetPlayer().Suicide()); + } + + Round = 2; + + DefaultColorIds.DoIf(x => x.Key != null && x.Value is >= byte.MinValue and <= byte.MaxValue, x => x.Key.RpcSetColor((byte)x.Value)); + + yield return NotifyEveryone("TMG.Notify.Round", 2, 2); + if (Stop) yield break; + + yield return NotifyEveryone("TMG.Tutorial.Round2", 9, SuperPointsToNormalPointsMultiplier); + if (Stop) yield break; + + for (int i = 0; i < 3; i++) + { + AuctionValue = IRandom.Instance.Next(1, 11); + Pick = Main.AllAlivePlayerControls.IntersectBy(Points.Keys, x => x.PlayerId).ToDictionary(x => x.PlayerId, x => IRandom.Instance.Next(Points[x.PlayerId] + 1)); + ProceedingInCountdownEndTS = Utils.TimeStamp + TimeForEachPickInRound2; + float timer = TimeForEachPickInRound2; + + ShowSuffixOtherThanPoints = true; + Utils.NotifyRoles(); + + while (timer > 0f) + { + timer -= Time.deltaTime; + if (Stop) yield break; + yield return null; + + if ((int)timer % 10 == 0 && LastTimeWarning + 2 < Utils.TimeStamp) + { + LastTimeWarning = Utils.TimeStamp; + Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.TimeLeft"), (int)timer), title: Translator.GetString("TMG.Message.TimeLeftTitle")); + } + } + + ShowSuffixOtherThanPoints = false; + + Main.AllAlivePlayerControls.Do(x => Points[x.PlayerId] -= Pick[x.PlayerId]); + int highestBid = Pick.Values.Max(); + List auctionWinners = Pick.Where(x => x.Value == highestBid).Select(x => x.Key).ToList(); + auctionWinners.ForEach(x => SuperPoints[x] += AuctionValue); + + yield return NotifyEveryone("TMG.Notify.AuctionEnd", 4, highestBid, string.Join(" & ", auctionWinners.ConvertAll(x => x.ColoredPlayerName())), AuctionValue); + if (Stop) yield break; + } + + int lowestSuperPoints = SuperPoints.Values.Min(); + Main.AllAlivePlayerControls.Join(SuperPoints, x => x.PlayerId, x => x.Key, (pc, kvp) => (pc, points: kvp.Value)).DoIf(x => x.points == lowestSuperPoints, x => x.pc.Suicide()); + + yield return NotifyEveryone("TMG.Notify.ItemPurchasingBegins", 6, TimeForItemPurchasingInRound2); + if (Stop) yield break; + + Item[] items = Enum.GetValues(); + int[] itemIds = items.Select(x => (int)x).ToArray(); + + foreach (PlayerControl pc in Main.AllAlivePlayerControls) + ItemIds[pc.PlayerId] = items.Zip(itemIds.Shuffle()).ToDictionary(x => x.First, x => x.Second); + + AuctionValue = 0; + ShowSuffixOtherThanPoints = true; + Utils.NotifyRoles(); + + ProceedingInCountdownEndTS = Utils.TimeStamp + TimeForItemPurchasingInRound2; + float timer2 = TimeForItemPurchasingInRound2; + + while (timer2 > 0f) + { + timer2 -= Time.deltaTime; + if (Stop) yield break; + yield return null; + + if ((int)timer2 % 10 == 0 && LastTimeWarning + 2 < Utils.TimeStamp) + { + LastTimeWarning = Utils.TimeStamp; + Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.TimeLeft"), (int)timer2), title: Translator.GetString("TMG.Message.TimeLeftTitle")); + } + + if (Main.AllAlivePlayerControls.Length == AmReady.Count) + { + ProceedingInCountdownEndTS = Utils.TimeStamp; + break; + } + } + + Main.AllAlivePlayerControls.ExceptBy(AmReady, x => x.PlayerId).Do(x => x.RpcChangeRoleBasis(CustomRoles.TMGPlayer)); + ShowSuffixOtherThanPoints = false; + + yield return NotifyEveryone("TMG.Notify.ItemPurchasingEnd", 3); + if (Stop) yield break; + + yield return NotifyEveryone("TMG.Notify.Round", 2, 3); + if (Stop) yield break; + + yield return NotifyEveryone("TMG.Tutorial.Round3", 6, Main.AllAlivePlayerControls.Length, MaxPlayersForRound4); + if (Stop) yield break; + + Main.AllAlivePlayerControls.Do(x => x.RpcChangeRoleBasis(CustomRoles.PhantomEHR)); + + while (true) + { + Pick = Main.AllAlivePlayerControls.ToDictionary(x => x.PlayerId, _ => IRandom.Instance.Next(1, Main.AllAlivePlayerControls.Length + 1)); + ProceedingInCountdownEndTS = Utils.TimeStamp + TimeForEachPickInRound3; + float timer = TimeForEachPickInRound3; + + ShowSuffixOtherThanPoints = true; + Utils.NotifyRoles(); + + while (timer > 0f) + { + timer -= Time.deltaTime; + if (Stop) yield break; + yield return null; + + if ((int)timer % 10 == 0 && LastTimeWarning + 2 < Utils.TimeStamp) + { + LastTimeWarning = Utils.TimeStamp; + Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.TimeLeft"), (int)timer), title: Translator.GetString("TMG.Message.TimeLeftTitle")); + } + } + + ShowSuffixOtherThanPoints = false; + + foreach (PlayerControl pc in Main.AllAlivePlayerControls) + { + int pick = Pick[pc.PlayerId]; + bool sameAsSomeoneElse = Main.AllAlivePlayerControls.Without(pc).FindFirst(x => Pick[x.PlayerId] == pick, out PlayerControl otherPc); + + if (!sameAsSomeoneElse) Points[pc.PlayerId] += pick; + + pc.Notify(string.Format(Translator.GetString(sameAsSomeoneElse ? "TMG.Notify.SamePick" : "TMG.Notify.Pick"), pick, sameAsSomeoneElse ? otherPc.PlayerId.ColoredPlayerName() : string.Empty)); + } + + yield return new WaitForSeconds(3f); + if (Stop) yield break; + NameNotifyManager.Reset(); + + int lowestScore = Points.Values.Min(); + Main.AllAlivePlayerControls.Join(Points, x => x.PlayerId, x => x.Key, (pc, kvp) => (pc, points: kvp.Value)).DoIf(x => x.points == lowestScore, x => x.pc.Suicide()); + + yield return NotifyEveryone("TMG.Notify.Round3NumPlayersLeft", 3, Main.AllAlivePlayerControls.Length, MaxPlayersForRound4); + if (Stop) yield break; + + if (Main.AllAlivePlayerControls.Length <= MaxPlayersForRound4) break; + } + + Round = 4; + + yield return NotifyEveryone("TMG.Notify.Round", 2, 4); + if (Stop) yield break; + + yield return NotifyEveryone("TMG.Tutorial.Round4", 20); + if (Stop) yield break; + + while (true) + { + PlayerControl.LocalPlayer.NoCheckStartMeeting(null, true); + yield return new WaitForSeconds(8f); + if (!GameStates.InGame || GameStates.IsLobby || GameStates.IsEnded) yield break; + + WinningBriefcaseHolderId = Main.AllAlivePlayerControls.RandomElement().PlayerId; + + Utils.SendMessage("\n", WinningBriefcaseHolderId, Translator.GetString("TMG.Message.YouHoldTheWinningBriefcase")); + Main.AllAlivePlayerControls.Select(x => x.PlayerId).Without(WinningBriefcaseHolderId).Do(x => Utils.SendMessage("\n", x, Translator.GetString("TMG.Message.YouHoldAnEmptyBriefcase"))); + + while (GameStates.IsMeeting || ExileController.Instance || AntiBlackout.SkipTasks) + { + yield return new WaitForSeconds(1f); + if (!GameStates.InGame || GameStates.IsLobby || GameStates.IsEnded) yield break; + } + + yield return new WaitForSeconds(1f); + + while (GameStates.IsMeeting || ExileController.Instance || AntiBlackout.SkipTasks) + { + yield return new WaitForSeconds(1f); + if (!GameStates.InGame || GameStates.IsLobby || GameStates.IsEnded) yield break; + } + + yield return new WaitForSeconds(1f); + + while (GameStates.IsMeeting || ExileController.Instance || AntiBlackout.SkipTasks) + { + yield return new WaitForSeconds(1f); + if (!GameStates.InGame || GameStates.IsLobby || GameStates.IsEnded) yield break; + } + + yield return new WaitForSeconds(1f); + + aapc = Main.AllAlivePlayerControls; + + if (aapc.Length <= 2) + { + if (aapc.Length >= 1) + { + WinningBriefcaseHolderId = aapc.RandomElement().PlayerId; + Round4PlacesFromLast.Add(WinningBriefcaseHolderId); + + if (aapc.Length == 2) + Round4PlacesFromFirst.Add(aapc.Select(x => x.PlayerId).Without(WinningBriefcaseHolderId).Single()); + + yield return NotifyEveryone("TMG.Notify.Round4EndLastHolder", 3, WinningBriefcaseHolderId.ColoredPlayerName()); + } + + break; + } + } + + EjectedPlayers.ToValidPlayers().FindAll(x => !x.IsAlive()).ForEach(x => x.RpcRevive()); + + yield return NotifyEveryone("TMG.Notify.Round4End", 3); + if (Stop) yield break; + + List ranking = []; + ranking.AddRange(Round4PlacesFromFirst); + Round4PlacesFromLast.Reverse(); + ranking.AddRange(Round4PlacesFromLast); + + string join = string.Join('\n', ranking.Select((x, i) => $"{i + 1}. {x.ColoredPlayerName()}")); + NameNotifyManager.Reset(); + Main.AllAlivePlayerControls.NotifyPlayers(join, 100f); + ProceedingInCountdownEndTS = Utils.TimeStamp + 5; + yield return new WaitForSeconds(5f); + if (Stop) yield break; + + Dictionary round4Points = ranking.ToDictionary(x => x, x => (ranking.Count - ranking.IndexOf(x)) * 10); + + join = string.Join('\n', round4Points.Select(x => $"{x.Key.ColoredPlayerName()}: +{x.Value}")); + yield return NotifyEveryone("TMG.Notify.Round4PointGain", 7, join); + if (Stop) yield break; + + foreach (PlayerControl pc in Main.AllAlivePlayerControls) + { + int superPoints = SuperPoints[pc.PlayerId]; + + for (int i = 0; i < superPoints; i++) + Points[pc.PlayerId] += SuperPointsToNormalPointsMultiplier; + } + + yield return NotifyEveryone("TMG.Notify.SuperPointConversion", 4, SuperPointsToNormalPointsMultiplier); + if (Stop) yield break; + + yield return NotifyEveryone("TMG.Notify.TheWinnerIs", 2); + + int highestPoints = Points.Values.Max(); + HashSet winners = Points.Where(x => x.Value == highestPoints).Select(x => x.Key).ToHashSet(); + CustomWinnerHolder.WinnerIds = winners; + } + + private static IEnumerator NotifyEveryone(string key, int time, params object[] args) + { + NameNotifyManager.Reset(); + Main.AllAlivePlayerControls.NotifyPlayers(string.Format(Translator.GetString(key), args), 100f); + ProceedingInCountdownEndTS = Utils.TimeStamp + time; + yield return new WaitForSeconds(time); + NameNotifyManager.Reset(); + } + + private static IEnumerator PreventMovingOutOfGroupRooms() + { + while (Round == 1 && !GameStates.IsEnded && GameStates.IsInTask && !ExileController.Instance) + { + foreach (PlayerControl pc in Main.AllAlivePlayerControls) + { + Group group = Groups[pc.PlayerId]; + SystemTypes groupRoom = GroupRooms[group]; + PlainShipRoom room = pc.GetPlainShipRoom(); + + if (room == null || room.RoomId != groupRoom) + pc.TP(RandomSpawn.SpawnMap.GetSpawnMap().Positions[groupRoom]); + } + + yield return new WaitForSeconds(2f); + } + } + + public static string GetEjectionMessage(NetworkedPlayerInfo exiled) + { + if (exiled == null) return string.Empty; + + EjectedPlayers.Add(exiled.PlayerId); + + if (WinningBriefcaseHolderId == exiled.PlayerId) + { + Round4PlacesFromFirst.Add(exiled.PlayerId); + return string.Format(Translator.GetString("TMG.EjectionText.HadWinningBriefcase"), exiled.PlayerId.ColoredPlayerName(), Round4PlacesFromFirst.IndexOf(exiled.PlayerId) + 1); + } + + Round4PlacesFromLast.Add(exiled.PlayerId); + return string.Format(Translator.GetString("TMG.EjectionText.HadEmptyBriefcase"), exiled.PlayerId.ColoredPlayerName()); + } + + public static void OnChat(PlayerControl pc, string text) + { + // TODO: Item purchasing + // TODO: Item using, including the stealing of briefcases + } + + public static void OnVanish(PlayerControl player) + { + switch (Round) + { + case 1: + { + Pick[player.PlayerId]++; + if (Pick[player.PlayerId] > 3) Pick[player.PlayerId] = 1; + break; + } + case 2 when AuctionValue == 0 && AmReady.Add(player.PlayerId): + { + player.RpcChangeRoleBasis(CustomRoles.TMGPlayer); + break; + } + case 2: + { + Pick[player.PlayerId]++; + if (Pick[player.PlayerId] > Points[player.PlayerId]) Pick[player.PlayerId] = 0; + break; + } + case 3: + { + Pick[player.PlayerId]++; + if (Pick[player.PlayerId] > Main.AllAlivePlayerControls.Length) Pick[player.PlayerId] = 1; + break; + } + } + + Utils.NotifyRoles(SpecifySeer: player, SpecifyTarget: player); + } + + private static string GetHexColor(this Group group) + { + return group switch + { + Group.Red => "#ff0000", + Group.Yellow => "#ffff00", + Group.Blue => "#00ffff", + Group.Green => "#00ff00", + Group.Tan => "#A88E8E", + Group.Rose => "#FAB8EB", + Group.Orange => "#ff8800", + _ => "#ffffff" + }; + } + + private static Color GetColor(this Group group) + { + return group switch + { + Group.Red => Color.red, + Group.Yellow => Color.yellow, + Group.Blue => Color.cyan, + Group.Green => Color.green, + Group.Tan => Palette.Brown, + Group.Rose => Color.magenta, + Group.Orange => Palette.Orange, + _ => Color.white + }; + } + + private static byte GetColorId(this Group group) + { + return group switch + { + Group.Red => 0, + Group.Yellow => 5, + Group.Blue => 10, + Group.Green => 11, + Group.Tan => 16, + Group.Rose => 13, + Group.Orange => 4, + _ => 7 + }; + } + + public static bool CheckForGameEnd(out GameOverReason reason) { } + + enum Item + { + BlindingGas, + MerchantDise, + Lasso, + DoubleModifier, + Fool, + MindDetective + } + + private enum Group + { + Red, + Yellow, + Blue, + Green, + Tan, + Rose, + Orange + } +} + +public class TMGPlayer : RoleBase +{ + public static bool On; + + public override bool IsEnable => On; + + public override void SetupCustomOption() { } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + } + + public override bool OnVanish(PlayerControl pc) + { + TheMindGame.OnVanish(pc); + return false; + } +} \ No newline at end of file diff --git a/Main.cs b/Main.cs index b24649513..3597b5755 100644 --- a/Main.cs +++ b/Main.cs @@ -699,6 +699,8 @@ public class Main : BasePlugin { CustomRoles.KOTZPlayer, "#ff0000" }, // Quiz { CustomRoles.QuizPlayer, "#CF2472" }, + // The Mind Game + { CustomRoles.TMGPlayer, "#ffff00" }, // Hide And Seek { CustomRoles.Seeker, "#ff1919" }, { CustomRoles.Hider, "#345eeb" }, diff --git a/Modules/AntiBlackout.cs b/Modules/AntiBlackout.cs index bf19807cf..95b19b43a 100644 --- a/Modules/AntiBlackout.cs +++ b/Modules/AntiBlackout.cs @@ -250,7 +250,7 @@ public static class AntiBlackout sender.WriteNetObject(pc); sender.Write((int)MurderResultFlags.Succeeded); sender.EndRpc(); - + hasValue[pc.PlayerId] = true; pc.ReactorFlash(0.2f); @@ -266,6 +266,7 @@ public static class AntiBlackout case CustomGameMode.HotPotato: case CustomGameMode.NaturalDisasters: case CustomGameMode.RoomRush: + case CustomGameMode.TheMindGame: case CustomGameMode.Quiz: { foreach (PlayerControl pc in Main.AllPlayerControls) @@ -283,7 +284,7 @@ public static class AntiBlackout sender.WriteNetObject(pc); sender.Write((int)MurderResultFlags.Succeeded); sender.EndRpc(); - + hasValue[pc.PlayerId] = true; pc.ReactorFlash(0.2f); @@ -306,7 +307,7 @@ public static class AntiBlackout foreach (PlayerControl pc in Main.AllPlayerControls) { CustomRpcSender sender = senders[pc.PlayerId]; - + if (pc.IsAlive()) { sender.RpcSetRole(pc, RoleTypes.Impostor, pc.OwnerId, noRpcForSelf: false); @@ -319,7 +320,7 @@ public static class AntiBlackout sender.WriteNetObject(pc); sender.Write((int)MurderResultFlags.Succeeded); sender.EndRpc(); - + hasValue[pc.PlayerId] = true; pc.ReactorFlash(0.2f); @@ -349,7 +350,7 @@ public static class AntiBlackout foreach (var kvp in senders) { if (!ids.Contains(kvp.Key)) continue; - + if (kvp.Value.stream.Length > 800) { kvp.Value.SendMessage(); diff --git a/Modules/CustomRoleSelector.cs b/Modules/CustomRoleSelector.cs index da75f1d4a..1fa9f49e0 100644 --- a/Modules/CustomRoleSelector.cs +++ b/Modules/CustomRoleSelector.cs @@ -20,7 +20,7 @@ internal static class CustomRoleSelector public static int AddTrackerNum; public static int AddPhantomNum; - public static Dictionary GameModeRoles = new() + public static readonly Dictionary GameModeRoles = new() { { CustomGameMode.SoloKombat, CustomRoles.KB_Normal }, { CustomGameMode.FFA, CustomRoles.Killer }, @@ -31,6 +31,7 @@ internal static class CustomRoleSelector { CustomGameMode.NaturalDisasters, CustomRoles.NDPlayer }, { CustomGameMode.RoomRush, CustomRoles.RRPlayer }, { CustomGameMode.KingOfTheZones, CustomRoles.KOTZPlayer }, + { CustomGameMode.TheMindGame, CustomRoles.TMGPlayer }, { CustomGameMode.Quiz, CustomRoles.QuizPlayer } }; diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 23205c436..9c902a1f7 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -64,7 +64,8 @@ internal static class CustomRolesHelper CustomRoles.NDPlayer or CustomRoles.RRPlayer or CustomRoles.KOTZPlayer or - CustomRoles.QuizPlayer; + CustomRoles.QuizPlayer or + CustomRoles.TMGPlayer; } public static RoleBase GetRoleClass(this CustomRoles role) @@ -381,6 +382,8 @@ internal static class CustomRolesHelper CustomRoles.RRPlayer => CustomRoles.Crewmate, // Quiz CustomRoles.QuizPlayer => CustomRoles.Crewmate, + // The Mind Game + CustomRoles.TMGPlayer => CustomRoles.Crewmate, _ => role.IsImpostor() ? CustomRoles.Impostor : CustomRoles.Crewmate }; diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 6dd465728..f960dd1fa 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -916,7 +916,7 @@ internal 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 and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters and not CustomGameMode.RoomRush and not CustomGameMode.Quiz and not CustomGameMode.AllInOne ? $" ({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 and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters and not CustomGameMode.RoomRush and not CustomGameMode.Quiz and not CustomGameMode.TheMindGame and not CustomGameMode.AllInOne ? $" ({player?.GetAllRoleName(forUser).RemoveHtmlTags().Replace('\n', ' ')})" : string.Empty); } public static string GetRoleColorCode(this PlayerControl player) @@ -1169,7 +1169,7 @@ internal static class ExtendedPlayerControl switch (Options.CurrentGameMode) { - case CustomGameMode.MoveAndStop or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush: + case CustomGameMode.MoveAndStop or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.TheMindGame: case CustomGameMode.Speedrun when !Speedrun.CanKill.Contains(pc.PlayerId): return false; case CustomGameMode.HotPotato: @@ -1238,6 +1238,7 @@ internal static class ExtendedPlayerControl CustomGameMode.NaturalDisasters => false, CustomGameMode.RoomRush => false, CustomGameMode.Quiz => false, + CustomGameMode.TheMindGame => false, CustomGameMode.AllInOne => false, CustomGameMode.Standard when CopyCat.Instances.Any(x => x.CopyCatPC.PlayerId == pc.PlayerId) => true, diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index dd0743481..45e115726 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -196,6 +196,11 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS case CustomGameMode.HideAndSeek: CustomHnS.ApplyGameOptions(opt, player); break; + case CustomGameMode.TheMindGame: + try { AURoleOptions.PhantomCooldown = 0.1f; } + catch (Exception e) { Utils.ThrowException(e); } + + goto case CustomGameMode.RoomRush; } switch (player.GetCustomRoleTypes()) diff --git a/Modules/GameState.cs b/Modules/GameState.cs index 9f1102f08..6450aa273 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -452,7 +452,7 @@ public class TaskState GameData.Instance.RecomputeTaskCounts(); Logger.Info($"TotalTaskCounts = {GameData.Instance.CompletedTasks}/{GameData.Instance.TotalTasks}", "TaskState.Update"); - if (Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.Quiz) + if (Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.Quiz or CustomGameMode.TheMindGame) player.Notify(Translator.GetString("DoingTasksIsPointlessInThisGameMode"), 10f); if (AllTasksCount == -1) Init(player); diff --git a/Modules/NameColorManager.cs b/Modules/NameColorManager.cs index 8485f1f07..e786a96ec 100644 --- a/Modules/NameColorManager.cs +++ b/Modules/NameColorManager.cs @@ -49,6 +49,7 @@ public static class NameColorManager case CustomGameMode.RoomRush: case CustomGameMode.NaturalDisasters: case CustomGameMode.MoveAndStop: + case CustomGameMode.TheMindGame: color = "#ffffff"; return true; case CustomGameMode.AllInOne: diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index 0c256334c..75aada96d 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -30,7 +30,8 @@ public enum CustomGameMode RoomRush = 0x0A, KingOfTheZones = 0x0B, Quiz = 0x0C, - AllInOne = 0x0D, + TheMindGame = 0x0D, + AllInOne = 0x0E, All = int.MaxValue } @@ -79,6 +80,7 @@ public static class Options "RoomRush", "KingOfTheZones", "Quiz", + "TheMindGame", "AllInOne" ]; @@ -836,7 +838,8 @@ public static class Options 9 => CustomGameMode.RoomRush, 10 => CustomGameMode.KingOfTheZones, 11 => CustomGameMode.Quiz, - 12 => CustomGameMode.AllInOne, + 12 => CustomGameMode.TheMindGame, + 13 => CustomGameMode.AllInOne, _ => CustomGameMode.Standard }; @@ -1589,6 +1592,8 @@ public static class Options KingOfTheZones.SetupCustomOption(); // Quiz Quiz.SetupCustomOption(); + // The Mind Game + TheMindGame.SetupCustomOption(); yield return null; diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 9f638e7b3..c36df2bdc 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -765,6 +765,7 @@ public static class Utils case CustomGameMode.NaturalDisasters: case CustomGameMode.RoomRush: case CustomGameMode.KingOfTheZones: + case CustomGameMode.TheMindGame: case CustomGameMode.Quiz: return false; case CustomGameMode.HideAndSeek: @@ -982,7 +983,7 @@ public static class Utils { switch (Options.CurrentGameMode) { - case CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz: + case CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.TheMindGame: case CustomGameMode.Standard when IsRevivingRoleAlive() && Main.DiedThisRound.Contains(PlayerControl.LocalPlayer.PlayerId): return PlayerControl.LocalPlayer.Is(CustomRoles.GM); case CustomGameMode.FFA or CustomGameMode.SoloKombat or CustomGameMode.MoveAndStop or CustomGameMode.HotPotato or CustomGameMode.Speedrun or CustomGameMode.AllInOne: @@ -1460,6 +1461,7 @@ public static class Utils case CustomGameMode.RoomRush: case CustomGameMode.NaturalDisasters: case CustomGameMode.KingOfTheZones: + case CustomGameMode.TheMindGame: case CustomGameMode.Quiz: case CustomGameMode.CaptureTheFlag: case CustomGameMode.Speedrun: @@ -2038,6 +2040,7 @@ public static class Utils CustomGameMode.NaturalDisasters => $"{modeText}\r\n{name}", CustomGameMode.RoomRush => $"{modeText}\r\n{name}", CustomGameMode.KingOfTheZones => $"{modeText}\r\n{name}", + CustomGameMode.TheMindGame => $"{modeText}\r\n{name}", CustomGameMode.AllInOne => $"{modeText}\r\n{name}", CustomGameMode.Speedrun => ColorString(GetRoleColor(CustomRoles.Speedrunner), $"{modeText}\r\n") + name, CustomGameMode.Quiz => ColorString(GetRoleColor(CustomRoles.QuizMaster), $"{modeText}\r\n") + name, @@ -2478,6 +2481,9 @@ public static class Utils case CustomGameMode.Quiz: additionalSuffixes.Add(Quiz.GetSuffix(seer)); break; + case CustomGameMode.TheMindGame: + additionalSuffixes.Add(TheMindGame.GetSuffix(seer, seer)); + break; case CustomGameMode.AllInOne: bool alive = seer.IsAlive(); if (alive && CustomGameMode.SoloKombat.IsActiveOrIntegrated()) additionalSuffixes.Add(SoloPVP.GetDisplayHealth(seer, true) + "\n"); @@ -2489,6 +2495,7 @@ public static class Utils if (CustomGameMode.RoomRush.IsActiveOrIntegrated()) additionalSuffixes.Add(RoomRush.GetSuffix(seer).Split('\n', splitFlags).Join(delimiter: " - ") + "\n"); if (CustomGameMode.KingOfTheZones.IsActiveOrIntegrated()) additionalSuffixes.Add(KingOfTheZones.GetSuffix(seer) + "\n"); if (CustomGameMode.Quiz.IsActiveOrIntegrated()) additionalSuffixes.Add(Quiz.GetSuffix(seer)); + if (CustomGameMode.TheMindGame.IsActiveOrIntegrated()) additionalSuffixes.Add(TheMindGame.GetSuffix(seer, seer)); break; } @@ -2505,7 +2512,7 @@ public static class Utils if ((CustomGameMode.FFA.IsActiveOrIntegrated() && FreeForAll.FFATeamMode.GetBool()) || CustomGameMode.HotPotato.IsActiveOrIntegrated()) seerRealName = seerRealName.ApplyNameColorData(seer, seer, forMeeting); - if (!forMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool() && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters and not CustomGameMode.RoomRush and not CustomGameMode.KingOfTheZones and not CustomGameMode.Quiz and not CustomGameMode.AllInOne) + if (!forMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool() && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters and not CustomGameMode.RoomRush and not CustomGameMode.KingOfTheZones and not CustomGameMode.Quiz and not CustomGameMode.TheMindGame and not CustomGameMode.AllInOne) { CustomTeamManager.CustomTeam team = CustomTeamManager.GetCustomTeam(seer.PlayerId); @@ -2543,7 +2550,7 @@ public static class Utils SelfSuffix.Append("\n\n" + GetString($"GameModeTutorial.{Options.CurrentGameMode}")); } - bool noRoleText = GameStates.IsLobby || Options.CurrentGameMode is CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz; + bool noRoleText = GameStates.IsLobby || Options.CurrentGameMode is CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.TheMindGame; // Combine the seer's job title and SelfTaskText with the seer's player name and SelfMark string selfRoleName = noRoleText ? string.Empty : $"{seer.GetDisplayRoleName()}{selfTaskText}"; @@ -2720,7 +2727,8 @@ public static class Utils if (IsRevivingRoleAlive() && Main.DiedThisRound.Contains(seer.PlayerId)) targetRoleText = string.Empty; - if (Options.CurrentGameMode is CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz) targetRoleText = string.Empty; + if (Options.CurrentGameMode is CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.TheMindGame) + targetRoleText = string.Empty; if (!GameStates.IsLobby) { @@ -2813,6 +2821,9 @@ public static class Utils case CustomGameMode.CaptureTheFlag: additionalSuffixes.Add(CaptureTheFlag.GetSuffixText(seer, target)); break; + case CustomGameMode.TheMindGame: + additionalSuffixes.Add(TheMindGame.GetSuffix(seer, target)); + break; } if (MeetingStates.FirstMeeting && Main.ShieldPlayer == target.FriendCode && !string.IsNullOrEmpty(target.FriendCode) && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.FFA or CustomGameMode.Speedrun) @@ -3553,6 +3564,9 @@ public static class Utils case CustomGameMode.Quiz: summary = $"{ColorString(Main.PlayerColors[id], name)} - {Quiz.GetStatistics(id)}"; break; + case CustomGameMode.TheMindGame: + summary = $"{ColorString(Main.PlayerColors[id], name)} - {TheMindGame.GetStatistics(id)}"; + break; case CustomGameMode.AllInOne: string survivalTimeText = !Main.PlayerStates[id].IsDead ? string.Empty : $" ({GetString("SurvivedTimePrefix")}: <#f542ad>{RoomRush.GetSurvivalTime(id)}s)"; summary = $"{ColorString(Main.PlayerColors[id], name)} -{taskCount}{GetKillCountText(id, true)} ({GetVitalText(id, true)}){survivalTimeText}"; diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index dd9bb47e9..47c6deb3d 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -509,6 +509,11 @@ internal static class GameEndChecker Predicate = new QuizGameEndPredicate(); } + public static void SetPredicateToTheMindGame() + { + Predicate = new TheMindGameGameEndPredicate(); + } + public static void SetPredicateToAllInOne() { Predicate = new AllInOneGameEndPredicate(); @@ -1017,6 +1022,20 @@ internal static class GameEndChecker } } + private class TheMindGameGameEndPredicate : GameEndPredicate + { + public override bool CheckForGameEnd(out GameOverReason reason) + { + reason = GameOverReason.ImpostorsByKill; + return WinnerIds.Count <= 0 && CheckGameEndByLivingPlayers(out reason); + } + + private static bool CheckGameEndByLivingPlayers(out GameOverReason reason) + { + return TheMindGame.CheckForGameEnd(out reason); + } + } + private class AllInOneGameEndPredicate : GameEndPredicate { public override bool CheckForGameEnd(out GameOverReason reason) diff --git a/Patches/GameOptionsMenuPatch.cs b/Patches/GameOptionsMenuPatch.cs index 36e3351a5..2802b2eea 100644 --- a/Patches/GameOptionsMenuPatch.cs +++ b/Patches/GameOptionsMenuPatch.cs @@ -1127,6 +1127,7 @@ public static class GameSettingMenuPatch [CustomGameMode.RoomRush] = Team.Neutral.GetColor(), [CustomGameMode.KingOfTheZones] = Color.red, [CustomGameMode.Quiz] = Utils.GetRoleColor(CustomRoles.QuizMaster), + [CustomGameMode.TheMindGame] = Color.yellow, [CustomGameMode.AllInOne] = ColorUtility.TryParseHtmlString("#f542ad", out c) ? c : Color.white }; diff --git a/Patches/HudPatch.cs b/Patches/HudPatch.cs index d50185417..2176d4cd4 100644 --- a/Patches/HudPatch.cs +++ b/Patches/HudPatch.cs @@ -466,6 +466,7 @@ internal static class SetHudActivePatch case CustomGameMode.MoveAndStop: case CustomGameMode.HotPotato: case CustomGameMode.Speedrun: + case CustomGameMode.TheMindGame: case CustomGameMode.NaturalDisasters: __instance.ReportButton?.ToggleVisible(false); __instance.SabotageButton?.ToggleVisible(false); @@ -842,6 +843,13 @@ internal static class TaskPanelBehaviourPatch finalText += Quiz.GetTaskBarText(); finalText += ""; break; + + case CustomGameMode.TheMindGame when AmongUsClient.Instance.AmHost: + + finalText += "\r\n\r\n\r\n"; + finalText += TheMindGame.GetTaskBarText(); + finalText += ""; + break; } __instance.taskText.text = finalText; diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index f433f13ad..c15c61d99 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -129,6 +129,16 @@ internal static class SetUpRoleTextPatch __instance.RoleBlurbText.text = GetString("QuizPlayerInfo"); break; } + case CustomGameMode.TheMindGame: + { + Color color = Color.yellow; + __instance.YouAreText.transform.gameObject.SetActive(false); + __instance.RoleText.text = GetString("TMGPlayer"); + __instance.RoleText.color = color; + __instance.RoleBlurbText.color = color; + __instance.RoleBlurbText.text = GetString("TMGPlayerInfo"); + break; + } case CustomGameMode.AllInOne: { Color color = ColorUtility.TryParseHtmlString("#f542ad", out Color c) ? c : new(255, 255, 255, 255); @@ -742,6 +752,15 @@ internal static class BeginCrewmatePatch __instance.ImpostorText.text = GetString("QuizPlayerInfo"); break; } + case CustomGameMode.TheMindGame: + { + __instance.TeamTitle.text = GetString("TMGPlayer"); + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = Color.yellow; + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Impostor); + __instance.ImpostorText.gameObject.SetActive(true); + __instance.ImpostorText.text = GetString("TMGPlayerInfo"); + break; + } case CustomGameMode.AllInOne: { __instance.TeamTitle.text = GetString("AllInOne"); @@ -1065,6 +1084,9 @@ internal static class IntroCutsceneDestroyPatch if (CustomGameMode.MoveAndStop.IsActiveOrIntegrated()) MoveAndStop.OnGameStart(); + if (CustomGameMode.TheMindGame.IsActiveOrIntegrated()) + Main.Instance.StartCoroutine(TheMindGame.OnGameStart()); + LateTask.New(() => { if (CustomGameMode.CaptureTheFlag.IsActiveOrIntegrated()) diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index 02f061934..c6aea70be 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -396,9 +396,7 @@ internal static class CheckForEndVotingPatch { if (pc == exiledPlayer.Object) continue; - if (pc.IsImpostor()) - impnum++; - else if (Options.MadmateCountMode.GetValue() == 1 && pc.IsMadmate()) + if (pc.IsImpostor() || Options.MadmateCountMode.GetValue() == 1 && pc.IsMadmate()) impnum++; else if (pc.IsNeutralKiller()) neutralnum++; @@ -506,6 +504,8 @@ internal static class CheckForEndVotingPatch EndOfSession: + if (CustomGameMode.TheMindGame.IsActiveOrIntegrated()) + name = TheMindGame.GetEjectionMessage(exiledPlayer); name = name.Replace("color=", string.Empty) + ""; TempExiledPlayer = exiledPlayer; diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index 83cf2d0d3..f8303da25 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -962,6 +962,9 @@ internal static class StartGameHostPatch case CustomGameMode.Quiz: GameEndChecker.SetPredicateToQuiz(); break; + case CustomGameMode.TheMindGame: + GameEndChecker.SetPredicateToTheMindGame(); + break; case CustomGameMode.AllInOne: GameEndChecker.SetPredicateToAllInOne(); break; diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index 58c51f1a1..407034896 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -69,7 +69,7 @@ internal static class EndGamePatch byte killerId = value.GetRealKiller(); bool gmIsFm = Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.MoveAndStop; - bool gmIsFmhh = gmIsFm || Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.HideAndSeek or CustomGameMode.Speedrun or CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.AllInOne; + bool gmIsFmhh = gmIsFm || Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.HideAndSeek or CustomGameMode.Speedrun or CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.TheMindGame or CustomGameMode.AllInOne; 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))})"); } @@ -253,6 +253,13 @@ internal static class SetEverythingUpPatch winnerText.color = Main.PlayerColors[winnerId]; goto EndOfText; } + case CustomGameMode.TheMindGame: + { + __instance.BackgroundBar.material.color = Color.yellow; + winnerText.text = CustomWinnerHolder.WinnerIds.Select(x => x.ColoredPlayerName()).Join() + GetString("Win"); + winnerText.color = Color.yellow; + goto EndOfText; + } case CustomGameMode.AllInOne: { byte winnerId = CustomWinnerHolder.WinnerIds.FirstOrDefault(); @@ -467,6 +474,12 @@ internal static class SetEverythingUpPatch break; } + case CustomGameMode.TheMindGame: + { + IOrderedEnumerable list = cloneRoles.OrderByDescending(TheMindGame.GetPoints); + foreach (byte id in list.Where(EndGamePatch.SummaryText.ContainsKey)) sb.Append('\n').Append(EndGamePatch.SummaryText[id]); + break; + } default: { foreach (byte id in cloneRoles) diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 25b251965..8a2c534da 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -244,6 +244,7 @@ internal static class CheckMurderPatch if (HotPotato.CanPassViaKillButton && holderID == killer.PlayerId && lastHolderID != target.PlayerId) HotPotato.FixedUpdatePatch.PassHotPotato(target, false); return false; + case CustomGameMode.TheMindGame: case CustomGameMode.MoveAndStop: case CustomGameMode.RoomRush: case CustomGameMode.NaturalDisasters: @@ -1843,6 +1844,9 @@ internal static class FixedUpdatePatch case CustomGameMode.Quiz when self: additionalSuffixes.Add(Quiz.GetSuffix(seer)); break; + case CustomGameMode.TheMindGame: + additionalSuffixes.Add(TheMindGame.GetSuffix(seer, target)); + break; case CustomGameMode.AllInOne: if (alive && CustomGameMode.SoloKombat.IsActiveOrIntegrated()) additionalSuffixes.Add(SoloPVP.GetDisplayHealth(target, self)); if (self && alive && CustomGameMode.MoveAndStop.IsActiveOrIntegrated()) additionalSuffixes.Add("\n" + MoveAndStop.GetSuffixText(seer) + "\n"); @@ -2098,6 +2102,7 @@ internal static class CoEnterVentPatch case CustomGameMode.CaptureTheFlag: case CustomGameMode.NaturalDisasters: case CustomGameMode.KingOfTheZones: + case CustomGameMode.TheMindGame: case CustomGameMode.Quiz: LateTask.New(() => __instance.RpcBootFromVent(id), 0.5f, log: false); return true; diff --git a/Patches/ShipStatusPatch.cs b/Patches/ShipStatusPatch.cs index 6b877458c..052cb69f9 100644 --- a/Patches/ShipStatusPatch.cs +++ b/Patches/ShipStatusPatch.cs @@ -241,7 +241,7 @@ internal static class CloseDoorsPatch { public static bool Prefix( /*ShipStatus __instance, */ [HarmonyArgument(0)] SystemTypes room) { - bool allow = !Options.DisableSabotage.GetBool() && Options.CurrentGameMode is not CustomGameMode.SoloKombat and not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters and not CustomGameMode.RoomRush and not CustomGameMode.KingOfTheZones and not CustomGameMode.Quiz and not CustomGameMode.AllInOne; + bool allow = !Options.DisableSabotage.GetBool() && Options.CurrentGameMode is not CustomGameMode.SoloKombat and not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters and not CustomGameMode.RoomRush and not CustomGameMode.KingOfTheZones and not CustomGameMode.Quiz and not CustomGameMode.TheMindGame and not CustomGameMode.AllInOne; if (SecurityGuard.BlockSabo.Count > 0) allow = false; if (Options.DisableCloseDoor.GetBool()) allow = false; diff --git a/Patches/TaskAssignPatch.cs b/Patches/TaskAssignPatch.cs index cdb40ca4a..66197bead 100644 --- a/Patches/TaskAssignPatch.cs +++ b/Patches/TaskAssignPatch.cs @@ -177,7 +177,7 @@ internal static class RpcSetTasksPatch } // GM and Lazy Guy have no tasks - if (pc.Is(CustomRoles.GM) || pc.Is(CustomRoles.Needy) || Options.CurrentGameMode is CustomGameMode.SoloKombat or CustomGameMode.FFA or CustomGameMode.HotPotato or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.Quiz or CustomGameMode.CaptureTheFlag or CustomGameMode.KingOfTheZones) + if (pc.Is(CustomRoles.GM) || pc.Is(CustomRoles.Needy) || Options.CurrentGameMode is CustomGameMode.SoloKombat or CustomGameMode.FFA or CustomGameMode.HotPotato or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.Quiz or CustomGameMode.CaptureTheFlag or CustomGameMode.KingOfTheZones or CustomGameMode.TheMindGame) { hasCommonTasks = false; numShortTasks = 0; diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 8ea19589d..82ac7ccf5 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -4950,6 +4950,49 @@ "KOTZPlayer": "KING OF THE ZONES", "KOTZPlayerInfo": "CONTROL ZONES TO WIN", "KOTZPlayerInfoLong": "CONTROL ZONES TO WIN", + "TheMindGame": "THE MIND GAME", + "ModeTheMindGame": "Gamemode: The Mind Game", + "ModeDescribe.TheMindGame": "", + // TODO: Add description + "TMG.Tutorial.Basics": "The Mind Game has 4 rounds.\\nEach round's goal will be explained as it begins.\\nGet as many points as possible - highest score wins!", + "TMG.Tutorial.Round1": "Pick a number 1-3 by clicking the Vanish button\\nYour number appears below your name and cycles with each click.\\nYou have {0} seconds to lock in your choice.\\nThis is how many points you demand.\\nIf another player picks the same number, neither of you get points.\\nReach {1} points in your group to move on.\\nThe lowest scorer(s) in each group are eliminated.\\nIf less than {2} players advance, the round repeats.", + "TMG.Tutorial.Round2": "3 hidden-bid auctions are held for super points.\\nBid using your normal points with the Vanish button (same as Round 1).\\nHighest bidder(s) win(s) the super points, others still lose their bid.\\nSuper points are used to buy items in the next round and count toward your final score (1 super point = {0} normal points).\\nAfter all auctions, players with the lowest super points are eliminated.", + "TMG.Notify.Round": "Round {0}", + "TMG.Notify.SamePick": "<#ff5555>{1} has also picked {0}!\\nNeither of you get points.", + "TMG.Notify.Pick": "<#00ffa5>Success! The {0} points are yours!", + "TMG.Notify.Round1NumPlayersAdvancing": "{0}/{1} players have enough points to advance. (Minimum: {2})", + "TMG.Notify.AuctionEnd": "The highest bid was {0} by {1}!\\nThey won {2} super points!", + "TMG.Notify.ItemPurchasingBegins": "Now you have {0}s to buy items with your super points!\\nPress Vanish to signal that you're done.\\nThe items can be used right away!", + "TMG.Notify.ItemPurchasingEnd": "The item purchasing phase is over!\\nThe remaining super points will contribute to your final score.", + "TMG.Tutorial.Round3": "Same rules as Round 1, but now you can use your items.\\nNo groups, everyone plays together.\\nPick a number from 1 to {0} using the Vanish button.\\nLowest scorers are eliminated until only {1} players remain for Round 4.", + "TMG.Notify.Round3NumPlayersLeft": "{0} players remain. (Max {1} players for Round 4)", + "TMG.Tutorial.Round4": "One player gets the winning briefcase, others get empty ones.\\nGoal: Get ejected while holding the winning briefcase!\\n\\nIf ejected with the winning briefcase: you lock in your placement.\\nIf ejected with an empty one: you're ranked from the bottom.\\n\\nAfter each ejection, a meeting is triggered and the briefcase is shuffled.\\nWhen only 2 players remain, the one holding the winning briefcase automatically loses.\\n\\nBriefcases can be stolen using Lasso.\\nIf yours is stolen, you'll be notified.\\nUse `/jp {id}` to guess who took it and try to steal it back.", + "TMG.Notify.Round4EndLastHolder": "{0} would receive the last winning briefcase", + "TMG.Notify.Round4End": "The final round has ended.\\nLet's see the ranking of the players from Round 4!", + "TMG.Notify.Round4PointGain": "Let's convert the places to points!\\n\\n{0}", + "TMG.Notify.SuperPointConversion": "Your super points have been converted to normal points.\\n1 super point is worth {0} normal points.", + "TMG.Notify.TheWinnerIs": "And the winner is....", + "TMG.EjectionText.HadWinningBriefcase": "{0} had the <#00ff00>winning briefcase!\\nThey are ranked {1}.", + "TMG.EjectionText.HadEmptyBriefcase": "{0} had an empty briefcase.", + "TMG.Message.TimeLeft": "Proceeding in {0}s", + "TMG.Message.TimeLeftTitle": "<#ffff00>⚠ Time Is Ticking ⚠", + "TMG.Suffix.BuyItemHint": "To buy an item, type 'b' and then the item's ID. For example, 'b0' buys the first item.", + "TMG.Suffix.UseItemHint": "To use an item, type 'u', then the item's ID, then the player's ID who you want to use it on, separated by a space. For example, 'u0 3' uses the first item on the player who has the '3' ID.", + "TMG.Item.BlindingGas": "Blinding Gas", + "TMG.Item.MerchantDise": "Merchant-Dise", + "TMG.Item.Lasso": "Lasso", + "TMG.Item.DoubleModifier": "2x Modifier", + "TMG.Item.Fool": "Fool", + "TMG.Item.MindDetective": "Mind Detective", + "TMG.ItemDescription.BlindingGas": "Temporarily hides a player's normal point total from them and blocks feedback from point demanding.", + "TMG.ItemDescription.MerchantDise": "Prevents one player from buying items during this phase.", + "TMG.ItemDescription.Lasso": "Used in Round 4 to steal a player's briefcase.", + "TMG.ItemDescription.DoubleModifier": "Doubles point gain from point demanding.", + "TMG.ItemDescription.Fool": "Instantly causes a target player to lose {0} normal points.", + "TMG.ItemDescription.MindDetective": "Reveals the target's point demand. Has a chance to be inaccurate.", + "TMGPlayer": "THE MIND GAME", + "TMGPlayerInfo": "THINK WITH OTHERS' BRAINS TO WIN", + "TMGPlayerInfoLong": "THINK WITH OTHERS' BRAINS TO WIN", "Quiz": "QUIZ", "ModeQuiz": "Gamemode: Quiz", "ModeDescribe.Quiz": "In the Quiz game mode, you must answer questions correctly to survive. There are various difficulties with varying time limits. Each question has 5 answer choices: A, B, C, D, E. Each choice has a room name next to the answer. To pick an answer, you must enter the room corresponding to the answer. If you answer correctly, you survive. If you don't answer enough questions correctly, you eventually die or compete in a Free For All (FFA) match with the other players who got the questions wrong.",