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