Files
2026-09-17 18:40:21 +02:00

1034 lines
42 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using EHR.Modules;
using EHR.Roles;
using Hazel;
using InnerNet;
using UnityEngine;
namespace EHR.Gamemodes;
public static class TheMindGame
{
private static Dictionary<byte, int> Points = [];
private static Dictionary<byte, int> SuperPoints = [];
private static Dictionary<byte, Group> Groups = [];
private static Dictionary<Group, List<byte>> GroupPlayers = [];
private static List<SystemTypes> AllRooms = [];
private static Dictionary<Group, SystemTypes> GroupRooms = [];
private static Dictionary<byte, int> Pick = [];
private static HashSet<byte> AmReady = [];
private static Dictionary<byte, Dictionary<Item, int>> ItemIds = [];
private static Dictionary<byte, List<Item>> PlayerItems = [];
private static HashSet<byte> HiddenPoints = [];
private static HashSet<byte> CannotPurchase = [];
private static HashSet<byte> DoubleModifierActive = [];
private static long ProceedingInCountdownEndTS;
private static long LastTimeWarning;
private static bool ShowSuffixOtherThanPoints;
private static int AuctionValue;
private static byte WinningBriefcaseHolderId;
private static byte WinningBriefcaseLastHolderId;
private static List<byte> Round4PlacesFromFirst;
private static List<byte> Round4PlacesFromLast;
private static List<byte> EjectedPlayers;
private static bool PreventGameEnd;
private static int Round;
private static readonly Item[] AllItem = EnumHelper.GetValues<Item>();
// Settings
private static bool PlayersCanSeeOthersPoints = true;
private static int NumGroupsForRound1 = 5;
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<Item, int> ItemCosts = [];
private static int TimeForEachPickInRound3 = 30;
private static int MaxPlayersForRound4 = 5;
private static int MindDetectiveFailChance = 10;
private static OptionItem PlayersCanSeeOthersPointsOption;
private static OptionItem NumGroupsForRound1Option;
private static OptionItem TimeForEachPickInRound1Option;
private static OptionItem NumPointsToAdvanceInRound1Option;
private static OptionItem MinPlayersInRound2Option;
private static OptionItem SuperPointsToNormalPointsMultiplierOption;
private static OptionItem TimeForEachPickInRound2Option;
private static OptionItem TimeForItemPurchasingInRound2Option;
private static OptionItem FoolNumPointsLostOption;
private static OptionItem TimeForEachPickInRound3Option;
private static OptionItem MaxPlayersForRound4Option;
private static OptionItem MindDetectiveFailChanceOption;
private static readonly Dictionary<Item, OptionItem> ItemCostsOptions = [];
private static bool Stop => GameStates.IsMeeting || ExileController.Instance || !GameStates.InGame || GameStates.IsLobby || GameStates.IsEnded;
public static void SetupCustomOption()
{
var id = 69_221_001;
var color = Color.yellow;
const CustomGameMode gameMode = CustomGameMode.TheMindGame;
const TabGroup tab = TabGroup.GameSettings;
PlayersCanSeeOthersPointsOption = new BooleanOptionItem(id++, "TMG.Setting.PlayersCanSeeOthersPoints", true, tab)
.SetHeader(true)
.SetColor(color)
.SetGameMode(gameMode);
NumGroupsForRound1Option = new IntegerOptionItem(id++, "TMG.Setting.NumGroupsForRound1", new IntegerValueRule(1, 7, 1), 5, tab)
.SetColor(color)
.SetGameMode(gameMode);
TimeForEachPickInRound1Option = new IntegerOptionItem(id++, "TMG.Setting.TimeForEachPickInRound1", new IntegerValueRule(1, 60, 1), 8, tab)
.SetValueFormat(OptionFormat.Seconds)
.SetColor(color)
.SetGameMode(gameMode);
NumPointsToAdvanceInRound1Option = new IntegerOptionItem(id++, "TMG.Setting.NumPointsToAdvanceInRound1", new IntegerValueRule(1, 100, 1), 10, tab)
.SetColor(color)
.SetGameMode(gameMode);
MinPlayersInRound2Option = new IntegerOptionItem(id++, "TMG.Setting.MinPlayersInRound2", new IntegerValueRule(1, 15, 1), 10, tab)
.SetValueFormat(OptionFormat.Players)
.SetColor(color)
.SetGameMode(gameMode);
SuperPointsToNormalPointsMultiplierOption = new IntegerOptionItem(id++, "TMG.Setting.SuperPointsToNormalPointsMultiplier", new IntegerValueRule(1, 10, 1), 3, tab)
.SetValueFormat(OptionFormat.Multiplier)
.SetColor(color)
.SetGameMode(gameMode);
TimeForEachPickInRound2Option = new IntegerOptionItem(id++, "TMG.Setting.TimeForEachPickInRound2", new IntegerValueRule(1, 60, 1), 25, tab)
.SetValueFormat(OptionFormat.Seconds)
.SetColor(color)
.SetGameMode(gameMode);
TimeForItemPurchasingInRound2Option = new IntegerOptionItem(id++, "TMG.Setting.TimeForItemPurchasingInRound2", new IntegerValueRule(1, 60, 1), 40, tab)
.SetValueFormat(OptionFormat.Seconds)
.SetColor(color)
.SetGameMode(gameMode);
FoolNumPointsLostOption = new IntegerOptionItem(id++, "TMG.Setting.FoolNumPointsLost", new IntegerValueRule(1, 100, 1), 5, tab)
.SetColor(color)
.SetGameMode(gameMode);
TimeForEachPickInRound3Option = new IntegerOptionItem(id++, "TMG.Setting.TimeForEachPickInRound3", new IntegerValueRule(1, 60, 1), 20, tab)
.SetValueFormat(OptionFormat.Seconds)
.SetColor(color)
.SetGameMode(gameMode);
MaxPlayersForRound4Option = new IntegerOptionItem(id++, "TMG.Setting.MaxPlayersForRound4", new IntegerValueRule(1, 15, 1), 5, tab)
.SetValueFormat(OptionFormat.Players)
.SetColor(color)
.SetGameMode(gameMode);
MindDetectiveFailChanceOption = new IntegerOptionItem(id++, "TMG.Setting.MindDetectiveFailChance", new IntegerValueRule(0, 100, 1), 5, tab)
.SetValueFormat(OptionFormat.Percent)
.SetColor(color)
.SetGameMode(gameMode);
foreach (Item item in AllItem)
{
int defaultValue = item switch
{
Item.BlindingGas => 5,
Item.MerchantDise => 2,
Item.Lasso => 6,
Item.DoubleModifier => 4,
Item.Fool => 3,
Item.MindDetective => 2,
_ => 5
};
var option = new IntegerOptionItem(id++, $"TMG.Setting.ItemCost.{item}", new IntegerValueRule(1, 100, 1), defaultValue, tab)
.SetHeader((int)item == 0)
.SetColor(color)
.SetGameMode(gameMode);
ItemCostsOptions[item] = option;
}
}
public static string GetStatistics(byte id)
{
return string.Format(Translator.GetString("TMG.EndScreen.Points"), GetPoints(id));
}
public static int GetPoints(byte id)
{
return Points.GetValueOrDefault(id, 0);
}
public static string GetTaskBarText()
{
long now = Utils.TimeStamp;
return ProceedingInCountdownEndTS < now ? string.Empty : string.Format(Translator.GetString("TMG.Message.TimeLeft"), ProceedingInCountdownEndTS - now);
}
public static string GetSuffix(PlayerControl seer, PlayerControl target)
{
if (!target.IsAlive() || !Main.IntroDestroyed || Round is 0 or 5) return string.Empty;
var sb = new StringBuilder("<#ffffff>");
bool self = seer.PlayerId == target.PlayerId;
int points = Points[target.PlayerId];
int superPoints = SuperPoints[target.PlayerId];
if (!self)
{
sb.Append($"ID {target.PlayerId}");
sb.Append('\n');
}
if ((!self || !HiddenPoints.Contains(seer.PlayerId)) && (self || PlayersCanSeeOthersPoints))
sb.Append(string.Format(Translator.GetString("TMG.Suffix.Points"), points, superPoints));
if (!self) return sb.ToString();
if (ShowSuffixOtherThanPoints)
{
switch (Round)
{
case 1:
case 3:
sb.Append('\n');
sb.Append('\n');
sb.Append(string.Format(Translator.GetString("TMG.Suffix.YourPick"), Pick[seer.PlayerId]));
sb.Append('\n');
sb.Append(Translator.GetString("TMG.Suffix.ChangePickHint"));
break;
case 2 when AuctionValue != 0:
sb.Append('\n');
sb.Append('\n');
sb.Append(string.Format(Translator.GetString("TMG.Suffix.AuctionValue"), AuctionValue));
sb.Append('\n');
sb.Append(string.Format(Translator.GetString("TMG.Suffix.YourBid"), Pick[seer.PlayerId]));
sb.Append('\n');
sb.Append(Translator.GetString("TMG.Suffix.ChangeBidHint"));
break;
case 2:
sb.Append('\n');
sb.Append('\n');
sb.Append("<size=80%>");
sb.Append(AllItem.Join('\n', x => $"{Translator.GetString($"TMG.Item.{x}")} (ID {ItemIds[seer.PlayerId][x]}) ({string.Format(Translator.GetString("TMG.Suffix.ItemCost"), ItemCosts[x])}) - {Translator.GetString($"TMG.ItemDescription.{x}")}"));
sb.Append('\n');
sb.Append('\n');
sb.Append(Translator.GetString("TMG.Suffix.BuyItemHint"));
sb.Append("</size>");
break;
}
}
List<Item> items = PlayerItems[seer.PlayerId];
if (items.Count > 0)
{
sb.Append('\n');
sb.Append('\n');
sb.Append(Translator.GetString("TMG.Suffix.YourItems"));
sb.Append('\n');
sb.Append(items.GroupBy(x => x).Join(", ", x => $"{x.Count()}x {Translator.GetString($"TMG.Item.{x.Key}")} (ID {ItemIds[seer.PlayerId][x.Key]})"));
sb.Append('\n');
sb.Append('\n');
sb.Append(Translator.GetString("TMG.Suffix.UseItemHint"));
}
return sb.ToString().Trim();
}
public static IEnumerator OnGameStart()
{
Round = 0;
ShowSuffixOtherThanPoints = false;
yield return null;
var currentMap = Main.CurrentMap;
AllRooms = ShipStatus.Instance.AllRooms.Select(x => x.RoomId).Distinct().Shuffle();
AllRooms.Remove(SystemTypes.Hallway);
AllRooms.Remove(SystemTypes.Outside);
AllRooms.Remove(SystemTypes.Ventilation);
if (currentMap is MapNames.Skeld or MapNames.Dleks or MapNames.Polus) AllRooms.Remove(SystemTypes.LifeSupp);
if (currentMap is MapNames.Skeld or MapNames.Dleks) AllRooms.Remove(SystemTypes.UpperEngine);
if (currentMap is MapNames.Skeld or MapNames.Dleks) AllRooms.Remove(SystemTypes.LowerEngine);
if (currentMap is MapNames.MiraHQ) AllRooms.Remove(SystemTypes.Storage);
if (currentMap is MapNames.MiraHQ) AllRooms.Remove(SystemTypes.MedBay);
if (currentMap is MapNames.MiraHQ) AllRooms.Remove(SystemTypes.Admin);
if (currentMap is MapNames.MiraHQ or MapNames.Polus or MapNames.Airship) AllRooms.Remove(SystemTypes.Comms);
if (currentMap is MapNames.Polus) AllRooms.Remove(SystemTypes.Security);
if (currentMap is MapNames.Polus) AllRooms.Remove(SystemTypes.BoilerRoom);
if (currentMap is MapNames.Fungle) AllRooms.Remove(SystemTypes.SleepingQuarters);
if (currentMap is MapNames.Fungle) AllRooms.Remove(SystemTypes.FishingDock);
if (currentMap is MapNames.Fungle) AllRooms.Remove(SystemTypes.Greenhouse);
AllRooms.RemoveAll(x => x.ToString().Contains("Decontamination"));
if (SubmergedCompatibility.IsSubmerged()) AllRooms.RemoveAll(x => (byte)x > 135);
var aapc = Main.AllAlivePlayerControlsToList;
var pcCount = aapc.Count;
Points = aapc.ToDictionary(x => x.PlayerId, _ => 0);
SuperPoints = aapc.ToDictionary(x => x.PlayerId, _ => 0);
PlayerItems = aapc.ToDictionary(x => x.PlayerId, _ => new List<Item>());
Groups = [];
GroupPlayers = [];
GroupRooms = [];
Pick = [];
AmReady = [];
ItemIds = [];
HiddenPoints = [];
CannotPurchase = [];
DoubleModifierActive = [];
EjectedPlayers = [];
Round4PlacesFromFirst = [];
Round4PlacesFromLast = [];
WinningBriefcaseHolderId = byte.MaxValue;
WinningBriefcaseLastHolderId = byte.MaxValue;
PreventGameEnd = false;
AuctionValue = 0;
ItemCosts = ItemCostsOptions.ToDictionary(x => x.Key, x => x.Value.GetInt());
PlayersCanSeeOthersPoints = PlayersCanSeeOthersPointsOption.GetBool();
NumGroupsForRound1 = NumGroupsForRound1Option.GetInt();
TimeForEachPickInRound1 = TimeForEachPickInRound1Option.GetInt();
NumPointsToAdvanceInRound1 = NumPointsToAdvanceInRound1Option.GetInt();
MinPlayersInRound2 = MinPlayersInRound2Option.GetInt();
SuperPointsToNormalPointsMultiplier = SuperPointsToNormalPointsMultiplierOption.GetInt();
TimeForEachPickInRound2 = TimeForEachPickInRound2Option.GetInt();
TimeForItemPurchasingInRound2 = TimeForItemPurchasingInRound2Option.GetInt();
FoolNumPointsLost = FoolNumPointsLostOption.GetInt();
TimeForEachPickInRound3 = TimeForEachPickInRound3Option.GetInt();
MaxPlayersForRound4 = MaxPlayersForRound4Option.GetInt();
MindDetectiveFailChance = MindDetectiveFailChanceOption.GetInt();
if (MinPlayersInRound2 > pcCount)
MinPlayersInRound2 = pcCount;
{
IEnumerable<IEnumerable<PlayerControl>> groups = aapc.Partition(NumGroupsForRound1);
RandomSpawn.SpawnMap map = RandomSpawn.SpawnMap.GetSpawnMap();
GroupRooms = [];
Group group = default(Group);
foreach (IEnumerable<PlayerControl> players in groups)
{
(SystemTypes room, Vector2 location) = map.Positions.IntersectBy(AllRooms, x => x.Key).ExceptBy(GroupRooms.Values, x => x.Key).RandomElement();
GroupRooms[group] = room;
List<byte> ids = [];
foreach (PlayerControl pc in players)
{
Groups[pc.PlayerId] = group;
pc.TP(location);
ids.Add(pc.PlayerId);
}
GroupPlayers[group] = ids;
group++;
}
}
Round = 1;
Main.Instance.StartCoroutine(PreventMovingOutOfGroupRooms());
Main.AllAlivePlayerControlsToList.SetChatVisible(true);
yield return NotifyEveryone("TMG.Tutorial.Basics", 6);
if (Stop) yield break;
yield return NotifyEveryone("TMG.Notify.Round", 2, 1);
if (Stop) yield break;
yield return NotifyEveryone("TMG.Tutorial.Round1", 12, TimeForEachPickInRound1, NumPointsToAdvanceInRound1, Main.AllAlivePlayerControlsCount - MinPlayersInRound2);
if (Stop) yield break;
Main.EnumerateAlivePlayerControls().Do(x => x.RpcChangeRoleBasis(CustomRoles.PhantomEHR));
while (true)
{
Pick = aapc.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 && timer > 3f)
{
LastTimeWarning = Utils.TimeStamp;
Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.TimeLeft"), (int)timer), title: Translator.GetString("TMG.Message.TimeLeftTitle"));
LateTask.New(() => Utils.NotifyRoles(SpecifyTarget: Main.EnumerateAlivePlayerControls().MinBy(x => x.PlayerId)), 1f, log: false);
}
}
ShowSuffixOtherThanPoints = false;
foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls())
{
Group group = Groups[pc.PlayerId];
int pick = Pick[pc.PlayerId];
bool sameAsSomeoneElse = GroupPlayers[group].FindFirst(x => x != pc.PlayerId && Pick.TryGetValue(x, out int p) && p == 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 WaitForSecondsRealtime(3f);
if (Stop) yield break;
NameNotifyManager.Reset();
if (Points.Values.Any(x => x >= NumPointsToAdvanceInRound1)) break;
}
aapc = Main.AllAlivePlayerControlsToList;
aapc.Join(Points, x => x.PlayerId, x => x.Key, (pc, kvp) => (pc, points: kvp.Value)).OrderBy(x => x.points).SkipLast(MinPlayersInRound2).Do(x => x.pc.Suicide());
Round = 2;
yield return NotifyEveryone("TMG.Notify.Round", 2, 2);
if (Stop) yield break;
yield return NotifyEveryone("TMG.Tutorial.Round2", 10, SuperPointsToNormalPointsMultiplier);
if (Stop) yield break;
for (int i = 0; i < 3; i++)
{
AuctionValue = IRandom.Instance.Next(1, 11);
Pick = Main.EnumerateAlivePlayerControls().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"));
LateTask.New(() => Utils.NotifyRoles(SpecifyTarget: Main.EnumerateAlivePlayerControls().MinBy(x => x.PlayerId)), 1f, log: false);
}
}
ShowSuffixOtherThanPoints = false;
Main.EnumerateAlivePlayerControls().Do(x => Points[x.PlayerId] -= Pick[x.PlayerId]);
int highestBid = Pick.Values.Max();
List<byte> 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, auctionWinners.Join(" & ", x => x.ColoredPlayerName()), AuctionValue);
if (Stop) yield break;
}
int lowestSuperPoints = SuperPoints.Values.Min();
Main.EnumerateAlivePlayerControls().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 = AllItem;
int[] itemIds = items.Select(x => (int)x).ToArray();
foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls())
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"));
LateTask.New(() => Utils.NotifyRoles(SpecifyTarget: Main.EnumerateAlivePlayerControls().MinBy(x => x.PlayerId)), 1f, log: false);
}
if (Main.AllAlivePlayerControlsCount == AmReady.Count)
{
ProceedingInCountdownEndTS = Utils.TimeStamp;
break;
}
}
ShowSuffixOtherThanPoints = false;
yield return NotifyEveryone("TMG.Notify.ItemPurchasingEnd", 3);
if (Stop) yield break;
Round = 3;
yield return NotifyEveryone("TMG.Notify.Round", 2, 3);
if (Stop) yield break;
yield return NotifyEveryone("TMG.Tutorial.Round3", 6, Main.AllAlivePlayerControlsCount, MaxPlayersForRound4);
if (Stop) yield break;
while (true)
{
Pick = Main.EnumerateAlivePlayerControls().ToDictionary(x => x.PlayerId, _ => IRandom.Instance.Next(1, Main.AllAlivePlayerControlsCount + 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"));
LateTask.New(() => Utils.NotifyRoles(SpecifyTarget: Main.EnumerateAlivePlayerControls().MinBy(x => x.PlayerId)), 1f, log: false);
}
}
ShowSuffixOtherThanPoints = false;
foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls())
{
int pick = Pick[pc.PlayerId];
bool sameAsSomeoneElse = Main.EnumerateAlivePlayerControls().Without(pc).FindFirst(x => Pick.TryGetValue(x.PlayerId, out int p) && p == pick, out PlayerControl otherPc);
if (!sameAsSomeoneElse)
Points[pc.PlayerId] += DoubleModifierActive.Contains(pc.PlayerId) ? pick * 2 : pick;
if (HiddenPoints.Contains(pc.PlayerId)) continue;
pc.Notify(string.Format(Translator.GetString(sameAsSomeoneElse ? "TMG.Notify.SamePick" : "TMG.Notify.Pick"), pick, sameAsSomeoneElse ? otherPc.PlayerId.ColoredPlayerName() : string.Empty));
}
yield return new WaitForSecondsRealtime(3f);
if (Stop) yield break;
NameNotifyManager.Reset();
int lowestScore = Points.Values.Min();
Main.EnumerateAlivePlayerControls().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.AllAlivePlayerControlsCount, MaxPlayersForRound4);
if (Stop) yield break;
if (Main.AllAlivePlayerControlsCount <= 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;
PreventGameEnd = true;
while (true)
{
PlayerControl.LocalPlayer.NoCheckStartMeeting(null, true);
yield return new WaitForSecondsRealtime(8f);
if (!GameStates.InGame || GameStates.IsLobby || GameStates.IsEnded) yield break;
WinningBriefcaseHolderId = Main.EnumerateAlivePlayerControls().RandomElement().PlayerId;
Utils.SendMessage("\n", WinningBriefcaseHolderId, Translator.GetString("TMG.Message.YouHoldTheWinningBriefcase"), importance: MessageImportance.High);
Main.EnumerateAlivePlayerControls().Select(x => x.PlayerId).Without(WinningBriefcaseHolderId).Do(x => Utils.SendMessage("\n", x, Translator.GetString("TMG.Message.YouHoldAnEmptyBriefcase"), importance: MessageImportance.High));
while (GameStates.IsMeeting || ExileController.Instance || AntiBlackout.SkipTasks)
{
yield return new WaitForSecondsRealtime(1f);
if (!GameStates.InGame || GameStates.IsLobby || GameStates.IsEnded) yield break;
}
yield return new WaitForSecondsRealtime(1f);
while (GameStates.IsMeeting || ExileController.Instance || AntiBlackout.SkipTasks)
{
yield return new WaitForSecondsRealtime(1f);
if (!GameStates.InGame || GameStates.IsLobby || GameStates.IsEnded) yield break;
}
yield return new WaitForSecondsRealtime(1f);
while (GameStates.IsMeeting || ExileController.Instance || AntiBlackout.SkipTasks)
{
yield return new WaitForSecondsRealtime(1f);
if (!GameStates.InGame || GameStates.IsLobby || GameStates.IsEnded) yield break;
}
yield return new WaitForSecondsRealtime(1f);
aapc = Main.AllAlivePlayerControlsToList;
var countPC = aapc.Count;
if (countPC <= 2)
{
if (countPC >= 1)
{
WinningBriefcaseHolderId = aapc.RandomElement().PlayerId;
Round4PlacesFromLast.Add(WinningBriefcaseHolderId);
if (countPC == 2)
Round4PlacesFromFirst.Add(aapc.Select(x => x.PlayerId).Without(WinningBriefcaseHolderId).Single());
yield return NotifyEveryone("TMG.Notify.Round4EndLastHolder", 3, WinningBriefcaseHolderId.ColoredPlayerName());
}
break;
}
}
Round = 5;
HiddenPoints.Clear();
EjectedPlayers.ToValidPlayers().Where(x => !x.IsAlive()).Do(x => x.RpcRevive());
yield return NotifyEveryone("TMG.Notify.Round4End", 3);
if (Stop) yield break;
List<byte> 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.EnumerateAlivePlayerControls().NotifyPlayers(join, 100f);
ProceedingInCountdownEndTS = Utils.TimeStamp + 5;
yield return new WaitForSecondsRealtime(5f);
if (Stop) yield break;
Dictionary<byte, int> round4Points = ranking.ToDictionary(x => x, x => (ranking.Count - ranking.IndexOf(x)) * 10);
round4Points.Do(x => Points[x.Key] += x.Value);
join = round4Points.Join('\n', x => $"{x.Key.ColoredPlayerName()}: +{x.Value}");
yield return NotifyEveryone("TMG.Notify.Round4PointGain", 5, join);
if (Stop) yield break;
foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls())
{
int superPoints = SuperPoints[pc.PlayerId];
for (int i = 0; i < superPoints; i++)
Points[pc.PlayerId] += SuperPointsToNormalPointsMultiplier;
}
var w = Utils.CreateRPC(CustomRPC.TMGSync);
w.WritePacked(Points.Count);
foreach ((byte key, int value) in Points)
{
w.Write(key);
w.WritePacked(value);
}
Utils.EndRPC(w);
SuperPoints.SetAllValues(0);
yield return NotifyEveryone("TMG.Notify.SuperPointConversion", 4, SuperPointsToNormalPointsMultiplier);
if (Stop) yield break;
yield return NotifyEveryone("TMG.Notify.TheWinnerIs", 2);
PreventGameEnd = false;
int highestPoints = Points.Values.Max();
HashSet<byte> 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();
string str = Translator.GetString(key);
Main.EnumerateAlivePlayerControls().NotifyPlayers(args.Length == 0 ? str : string.Format(str, args), 100f);
ProceedingInCountdownEndTS = Utils.TimeStamp + time;
yield return new WaitForSecondsRealtime(time);
NameNotifyManager.Reset();
}
private static IEnumerator PreventMovingOutOfGroupRooms()
{
while (Round == 1 && !GameStates.IsEnded && GameStates.IsInTask && !ExileController.Instance)
{
foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls())
{
Group group = Groups[pc.PlayerId];
SystemTypes groupRoom = GroupRooms[group];
if (!pc.IsInRoom(groupRoom))
pc.TP(RandomSpawn.SpawnMap.GetSpawnMap().Positions[groupRoom]);
}
yield return new WaitForSecondsRealtime(2f);
}
}
public static string GetEjectionMessage(byte exiledPlayerId)
{
EjectedPlayers.Add(exiledPlayerId);
if (WinningBriefcaseHolderId == exiledPlayerId)
{
Round4PlacesFromFirst.Add(exiledPlayerId);
return string.Format(Translator.GetString("TMG.EjectionText.HadWinningBriefcase"), exiledPlayerId.ColoredPlayerName(), Round4PlacesFromFirst.IndexOf(exiledPlayerId) + 1);
}
Round4PlacesFromLast.Add(exiledPlayerId);
return string.Format(Translator.GetString("TMG.EjectionText.HadEmptyBriefcase"), exiledPlayerId.ColoredPlayerName());
}
public static void OnChat(PlayerControl pc, string text)
{
try
{
if (Round < 2 || !text.Any(x => x is >= '0' and <= '9') || text.Length < 2 || GameStates.IsLobby) return;
Utils.CheckServerCommand(ref text, out bool spamRequired);
switch (text[0])
{
case 'b':
{
if (CannotPurchase.Contains(pc.PlayerId))
{
Utils.SendMessage(Translator.GetString("TMG.Message.CannotPurchase"), pc.PlayerId, "<#ff0000>X</color>");
break;
}
var item = FindItemFromText();
if (!item.HasValue) break;
int superPoints = SuperPoints[pc.PlayerId];
int cost = ItemCosts[item.Value];
string itemName = Translator.GetString($"TMG.Item.{item.Value}");
if (cost > superPoints)
{
Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.NotEnoughSuperPoints"), itemName, cost, superPoints), pc.PlayerId, "<#ff0000>X</color>");
break;
}
if (item.Value == Item.DoubleModifier) DoubleModifierActive.Add(pc.PlayerId);
SuperPoints[pc.PlayerId] -= cost;
PlayerItems[pc.PlayerId].Add(item.Value);
Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.ItemPurchased"), itemName, cost, superPoints), pc.PlayerId, "<#00ff00>✓</color>");
Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc);
break;
}
case 'u':
{
var item = FindItemFromText();
if (!item.HasValue || !PlayerItems[pc.PlayerId].Remove(item.Value)) break;
switch (item.Value)
{
case Item.BlindingGas:
{
byte id = FindTargetIdFromText();
if (id == byte.MaxValue) break;
HiddenPoints.Add(id);
PlayerControl target = id.GetPlayer();
Utils.NotifyRoles(SpecifySeer: target, SpecifyTarget: target);
Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.ItemUsed"), Translator.GetString($"TMG.Item.{item.Value}")), pc.PlayerId, "<#00ff00>✓</color>");
break;
}
case Item.MerchantDise:
{
byte id = FindTargetIdFromText();
if (id == byte.MaxValue) break;
CannotPurchase.Add(id);
Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.ItemUsed"), Translator.GetString($"TMG.Item.{item.Value}")), pc.PlayerId, "<#00ff00>✓</color>");
break;
}
case Item.Lasso:
{
if (Round != 4 || !GameStates.IsMeeting)
{
Utils.SendMessage(Translator.GetString("TMG.Message.LassoOnlyInRound4"), pc.PlayerId, "<#ff0000>X</color>");
break;
}
byte id = FindTargetIdFromText();
if (id == byte.MaxValue) break;
if (!pc.IsModdedClient() && spamRequired)
Utils.SendMessage("\n", pc.PlayerId, Translator.GetString("NoSpamAnymoreUseCmd"));
if (id == WinningBriefcaseHolderId)
{
WinningBriefcaseLastHolderId = id;
WinningBriefcaseHolderId = pc.PlayerId;
LateTask.New(() =>
{
Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.LassoedWinningBriefcaseSelf"), id.ColoredPlayerName()), pc.PlayerId, "<#00ff00>✓</color>", importance: MessageImportance.High);
Utils.SendMessage(Translator.GetString("TMG.Message.LassoedWinningBriefcase"), id, "<#ffff00>⚠</color>");
}, 0.2f);
}
else
LateTask.New(() => Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.LassoedEmptyBriefcase"), id.ColoredPlayerName()), pc.PlayerId, "<#ffa500>-</color>", importance: MessageImportance.High), 0.2f);
break;
}
case Item.Fool:
{
byte id = FindTargetIdFromText();
if (id == byte.MaxValue) break;
Points[id] -= FoolNumPointsLost;
break;
}
case Item.MindDetective:
{
byte id = FindTargetIdFromText();
if (id == byte.MaxValue) break;
int pick = Pick[id];
if (IRandom.Instance.Next(100) < MindDetectiveFailChance)
pick = IRandom.Instance.Next(1, Main.AllAlivePlayerControlsCount + 1);
Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.MindDetective"), id.ColoredPlayerName(), pick), pc.PlayerId, "<#00ff00>✓</color>", importance: MessageImportance.High);
break;
}
}
break;
}
case 's':
{
if (WinningBriefcaseLastHolderId != pc.PlayerId)
{
if (Round == 4) Utils.SendMessage(Translator.GetString("TMG.Message.StealBackNotLastHolder"), pc.PlayerId, "<#ff0000>X</color>", importance: MessageImportance.High);
break;
}
byte id = FindTargetIdFromText(false);
if (id == byte.MaxValue) break;
if (id == WinningBriefcaseHolderId)
{
WinningBriefcaseHolderId = pc.PlayerId;
WinningBriefcaseLastHolderId = byte.MaxValue;
Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.StealBackWinningBriefcaseSelf"), id.ColoredPlayerName()), pc.PlayerId, "<#00ff00>✓</color>", importance: MessageImportance.High);
Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.StealBackWinningBriefcase"), pc.PlayerId.ColoredPlayerName()), id, "<#ffff00>⚠</color>", importance: MessageImportance.High);
}
else
{
WinningBriefcaseLastHolderId = byte.MaxValue;
Utils.SendMessage(string.Format(Translator.GetString("TMG.Message.StealBackEmptyBriefcase"), id.ColoredPlayerName()), pc.PlayerId, "<#ffa500>-</color>", importance: MessageImportance.High);
}
break;
}
}
}
catch (Exception e) { Utils.ThrowException(e); }
return;
Item? FindItemFromText()
{
for (int i = 1; i < text.Length; i++)
{
char letter = text[i];
switch (letter)
{
case ' ':
case < '0':
case > '9':
continue;
default:
int id = letter - '0';
Dictionary<Item, int> itemIds = ItemIds[pc.PlayerId];
if (id >= itemIds.Count) break;
return itemIds.GetKeyByValue(id);
}
}
return null;
}
byte FindTargetIdFromText(bool space = true)
{
int startIndex;
if (space)
{
int lastSpaceIndex = text.LastIndexOf(' ');
if (lastSpaceIndex == -1 || lastSpaceIndex == text.Length - 1) return byte.MaxValue;
startIndex = lastSpaceIndex + 1;
}
else
startIndex = 1;
string idString = text[startIndex..];
return !byte.TryParse(idString, out byte id) ? byte.MaxValue : id;
}
}
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);
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.AllAlivePlayerControlsCount) Pick[player.PlayerId] = 1;
break;
}
}
Utils.NotifyRoles(SpecifySeer: player, SpecifyTarget: player);
}
public static void ReceiveRPC(MessageReader reader)
{
switch (reader.ReadPackedInt32(), AmongUsClient.Instance.AmHost)
{
case (1, true):
{
OnChat(reader.ReadNetObject<PlayerControl>(), reader.ReadString());
break;
}
case (2, false):
{
Points = [];
int count = reader.ReadPackedInt32();
for (int i = 0; i < count; i++)
{
byte key = reader.ReadByte();
int value = reader.ReadPackedInt32();
Points[key] = value;
}
break;
}
}
}
public static bool CheckForGameEnd(out GameOverReason reason)
{
reason = GameOverReason.ImpostorsByKill;
if (PreventGameEnd) return false;
if (CustomWinnerHolder.WinnerIds.Count > 0) return true;
var aapc = Main.CachedAlivePlayerControls();
switch (aapc.Count)
{
case 0:
CustomWinnerHolder.ResetAndSetWinner(CustomWinner.None);
CustomWinnerHolder.WinnerIds = [PlayerControl.LocalPlayer.PlayerId];
Logger.Warn("Game ended with no players alive", "TMG");
return true;
case 1:
CustomWinnerHolder.WinnerIds = [aapc[0].PlayerId];
Logger.Warn("Game ended with one player alive", "TMG");
return true;
}
return false;
}
enum Item
{
BlindingGas,
MerchantDise,
Lasso,
DoubleModifier,
Fool,
MindDetective
}
[SuppressMessage("ReSharper", "UnusedMember.Local")]
private enum Group
{
Red,
Yellow,
Blue,
Green,
Tan,
Rose,
Orange
}
}
public class TMGPlayer : RoleBase
{
private 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;
}
}