mirror of
https://github.com/Gurge44/EndlessHostRoles.git
synced 2026-10-19 15:51:58 +00:00
1020 lines
41 KiB
C#
1020 lines
41 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using AmongUs.GameOptions;
|
|
using EHR.Modules;
|
|
using Hazel;
|
|
using UnityEngine;
|
|
using static EHR.Translator;
|
|
|
|
namespace EHR.Gamemodes;
|
|
|
|
public static class KingOfTheZones
|
|
{
|
|
private static OptionItem AutoSetNumTeams;
|
|
private static OptionItem PreferNumTeams;
|
|
private static OptionItem NumTeams;
|
|
private static OptionItem NumZones;
|
|
private static OptionItem ZonesMove;
|
|
private static OptionItem ZoneMoveTime;
|
|
private static OptionItem AllZonesMoveAtOnce;
|
|
private static OptionItem DowntimeAfterZoneMove;
|
|
public static OptionItem RespawnTime;
|
|
private static OptionItem TagCooldown;
|
|
private static OptionItem GameEndsByPoints;
|
|
private static OptionItem PointsToWin;
|
|
private static OptionItem GameEndsByTimeLimit;
|
|
private static OptionItem MaxGameLength;
|
|
private static OptionItem SpawnProtectionTime;
|
|
|
|
private static readonly string[] PreferNumTeamsOptions = ["KOTZ.PNTO.Less", "KOTZ.PNTO.More"];
|
|
private static readonly KOTZTeam[] AllKOTZTeam = EnumHelper.GetValues<KOTZTeam>();
|
|
private static readonly StringBuilder Suffix = new();
|
|
|
|
public static (UnityEngine.Color Color, string Team) WinnerData = (Color.white, "No one wins");
|
|
|
|
private static readonly Dictionary<MapNames, List<List<SystemTypes>>> DefaultZones = new()
|
|
{
|
|
[MapNames.Skeld] =
|
|
[
|
|
[SystemTypes.Cafeteria],
|
|
[SystemTypes.LowerEngine, SystemTypes.Weapons],
|
|
[SystemTypes.LowerEngine, SystemTypes.Admin, SystemTypes.Weapons],
|
|
[SystemTypes.LowerEngine, SystemTypes.UpperEngine, SystemTypes.Weapons, SystemTypes.Shields]
|
|
],
|
|
[MapNames.MiraHQ] =
|
|
[
|
|
[SystemTypes.LockerRoom],
|
|
[SystemTypes.Launchpad, SystemTypes.Balcony],
|
|
[SystemTypes.Launchpad, SystemTypes.Greenhouse, SystemTypes.Balcony],
|
|
[SystemTypes.Launchpad, SystemTypes.Reactor, SystemTypes.Greenhouse, SystemTypes.Balcony]
|
|
],
|
|
[MapNames.Polus] =
|
|
[
|
|
[SystemTypes.Office],
|
|
[SystemTypes.LifeSupp, SystemTypes.Laboratory],
|
|
[SystemTypes.LifeSupp, SystemTypes.Office, SystemTypes.Laboratory],
|
|
[SystemTypes.LifeSupp, SystemTypes.Electrical, SystemTypes.Laboratory, SystemTypes.Specimens]
|
|
],
|
|
[MapNames.Airship] =
|
|
[
|
|
[SystemTypes.MainHall],
|
|
[SystemTypes.CargoBay, SystemTypes.Cockpit],
|
|
[SystemTypes.CargoBay, SystemTypes.MainHall, SystemTypes.Cockpit],
|
|
[SystemTypes.Records, SystemTypes.Electrical, SystemTypes.Kitchen, SystemTypes.VaultRoom]
|
|
],
|
|
[MapNames.Fungle] =
|
|
[
|
|
[SystemTypes.Greenhouse],
|
|
[SystemTypes.Cafeteria, SystemTypes.Reactor],
|
|
[SystemTypes.FishingDock, SystemTypes.Comms, SystemTypes.Reactor],
|
|
[SystemTypes.FishingDock, SystemTypes.Dropship, SystemTypes.Comms, SystemTypes.Reactor]
|
|
],
|
|
[(MapNames)6] =
|
|
[
|
|
[(SystemTypes)SubmergedCompatibility.SubmergedSystemTypes.UpperCentral],
|
|
[(SystemTypes)SubmergedCompatibility.SubmergedSystemTypes.UpperCentral, (SystemTypes)SubmergedCompatibility.SubmergedSystemTypes.LowerCentral],
|
|
[(SystemTypes)SubmergedCompatibility.SubmergedSystemTypes.Research, SystemTypes.Admin, (SystemTypes)SubmergedCompatibility.SubmergedSystemTypes.UpperLobby],
|
|
[(SystemTypes)SubmergedCompatibility.SubmergedSystemTypes.UpperCentral, (SystemTypes)SubmergedCompatibility.SubmergedSystemTypes.LowerCentral, (SystemTypes)SubmergedCompatibility.SubmergedSystemTypes.Ballast, SystemTypes.Cafeteria]
|
|
]
|
|
};
|
|
|
|
private static Dictionary<byte, NetworkedPlayerInfo.PlayerOutfit> DefaultOutfits = [];
|
|
private static Dictionary<byte, KOTZTeam> PlayerTeams = [];
|
|
public static Dictionary<byte, long> RespawnTimes = [];
|
|
private static Dictionary<byte, long> SpawnProtectionTimes = [];
|
|
private static Dictionary<byte, int> PlayerPoints = [];
|
|
private static Dictionary<KOTZTeam, int> Points = [];
|
|
private static Dictionary<SystemTypes, KOTZTeam> ZoneDomination = [];
|
|
private static Dictionary<SystemTypes, long> ZoneMoveSchedules = [];
|
|
private static Dictionary<SystemTypes, long> ZoneDowntimeExpire = [];
|
|
private static List<SystemTypes> Zones = [];
|
|
private static HashSet<SystemTypes> AllRooms = [];
|
|
private static int TimeLeft;
|
|
private static long GameStartTS;
|
|
private static string LastShortInfo = string.Empty;
|
|
|
|
public static bool GameGoing;
|
|
public static readonly HashSet<string> PlayedFCs = [];
|
|
|
|
static KingOfTheZones()
|
|
{
|
|
DefaultZones[MapNames.Dleks] = DefaultZones[MapNames.Skeld];
|
|
}
|
|
|
|
public static int KCD => TagCooldown.GetInt();
|
|
public static int MaxGameTime => GameEndsByTimeLimit.GetBool() ? MaxGameLength.GetInt() : int.MaxValue;
|
|
public static int MaxGameTimeByPoints => GameEndsByPoints.GetBool() ? PointsToWin.GetInt() * 2 : MaxGameTime;
|
|
|
|
public static void SetupCustomOption()
|
|
{
|
|
var id = 69_219_001;
|
|
Color color = ColorUtility.TryParseHtmlString("#ff0000", out Color c) ? c : Color.red;
|
|
const CustomGameMode gameMode = CustomGameMode.KingOfTheZones;
|
|
|
|
AutoSetNumTeams = new BooleanOptionItem(id++, "KingOfTheZones.AutoSetNumTeams", true, TabGroup.GameSettings)
|
|
.SetHeader(true)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
PreferNumTeams = new StringOptionItem(id++, "KingOfTheZones.PreferNumTeams", PreferNumTeamsOptions, 1, TabGroup.GameSettings)
|
|
.SetParent(AutoSetNumTeams)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
NumTeams = new IntegerOptionItem(id++, "KingOfTheZones.NumTeams", new(2, 7, 1), 2, TabGroup.GameSettings)
|
|
.SetHeader(true)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
NumZones = new IntegerOptionItem(id++, "KingOfTheZones.NumZones", new(1, 4, 1), 1, TabGroup.GameSettings)
|
|
.SetHeader(true)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
ZonesMove = new BooleanOptionItem(id++, "KingOfTheZones.ZonesMove", true, TabGroup.GameSettings)
|
|
.SetHeader(true)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
ZoneMoveTime = new IntegerOptionItem(id++, "KingOfTheZones.ZoneMoveTime", new(10, 120, 5), 30, TabGroup.GameSettings)
|
|
.SetParent(ZonesMove)
|
|
.SetValueFormat(OptionFormat.Seconds)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
AllZonesMoveAtOnce = new BooleanOptionItem(id++, "KingOfTheZones.AllZonesMoveAtOnce", false, TabGroup.GameSettings)
|
|
.SetParent(ZonesMove)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
DowntimeAfterZoneMove = new IntegerOptionItem(id++, "KingOfTheZones.DowntimeAfterZoneMove", new(0, 30, 1), 5, TabGroup.GameSettings)
|
|
.SetParent(ZonesMove)
|
|
.SetValueFormat(OptionFormat.Seconds)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
RespawnTime = new IntegerOptionItem(id++, "KingOfTheZones.RespawnTime", new(1, 30, 1), 5, TabGroup.GameSettings)
|
|
.SetHeader(true)
|
|
.SetValueFormat(OptionFormat.Seconds)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
TagCooldown = new IntegerOptionItem(id++, "KingOfTheZones.TagCooldown", new(1, 60, 1), 10, TabGroup.GameSettings)
|
|
.SetHeader(true)
|
|
.SetValueFormat(OptionFormat.Seconds)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
GameEndsByPoints = new BooleanOptionItem(id++, "KingOfTheZones.GameEndsByPoints", true, TabGroup.GameSettings)
|
|
.SetHeader(true)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color)
|
|
.RegisterUpdateValueEvent((_, _, _) =>
|
|
{
|
|
if (!GameEndsByTimeLimit.GetBool() && !GameEndsByPoints.GetBool())
|
|
GameEndsByTimeLimit.SetValue(1);
|
|
})
|
|
.SetRunEventOnLoad(true);
|
|
|
|
PointsToWin = new IntegerOptionItem(id++, "KingOfTheZones.PointsToWin", new(30, 300, 10), 120, TabGroup.GameSettings)
|
|
.SetParent(GameEndsByPoints)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
GameEndsByTimeLimit = new BooleanOptionItem(id++, "KingOfTheZones.GameEndsByTimeLimit", true, TabGroup.GameSettings)
|
|
.SetHeader(true)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color)
|
|
.RegisterUpdateValueEvent((_, _, _) =>
|
|
{
|
|
if (!GameEndsByTimeLimit.GetBool() && !GameEndsByPoints.GetBool())
|
|
GameEndsByPoints.SetValue(1);
|
|
})
|
|
.SetRunEventOnLoad(true);
|
|
|
|
MaxGameLength = new IntegerOptionItem(id++, "KingOfTheZones.MaxGameLength", new(10, 900, 10), 300, TabGroup.GameSettings)
|
|
.SetParent(GameEndsByTimeLimit)
|
|
.SetValueFormat(OptionFormat.Seconds)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
|
|
SpawnProtectionTime = new IntegerOptionItem(id, "KingOfTheZones.SpawnProtectionTime", new(0, 30, 1), 5, TabGroup.GameSettings)
|
|
.SetHeader(true)
|
|
.SetValueFormat(OptionFormat.Seconds)
|
|
.SetGameMode(gameMode)
|
|
.SetColor(color);
|
|
}
|
|
|
|
extension(KOTZTeam team)
|
|
{
|
|
private Color GetColor()
|
|
{
|
|
return team switch
|
|
{
|
|
KOTZTeam.Red => Color.red,
|
|
KOTZTeam.Yellow => Color.yellow,
|
|
KOTZTeam.Blue => Color.cyan,
|
|
KOTZTeam.Green => Color.green,
|
|
KOTZTeam.Tan => Palette.Brown,
|
|
KOTZTeam.Rose => Color.magenta,
|
|
KOTZTeam.Orange => Palette.Orange,
|
|
_ => Color.white
|
|
};
|
|
}
|
|
|
|
private byte GetColorId()
|
|
{
|
|
return team switch
|
|
{
|
|
KOTZTeam.Red => 0,
|
|
KOTZTeam.Yellow => 5,
|
|
KOTZTeam.Blue => 10,
|
|
KOTZTeam.Green => 11,
|
|
KOTZTeam.Tan => 16,
|
|
KOTZTeam.Rose => 13,
|
|
KOTZTeam.Orange => 4,
|
|
_ => 7
|
|
};
|
|
}
|
|
}
|
|
|
|
public static void Init()
|
|
{
|
|
WinnerData = (Color.white, "No one wins");
|
|
|
|
AllRooms = ShipStatus.Instance.AllRooms.Select(x => x.RoomId).ToHashSet();
|
|
AllRooms.Remove(SystemTypes.Hallway);
|
|
AllRooms.Remove(SystemTypes.Outside);
|
|
if (SubmergedCompatibility.IsSubmerged()) AllRooms.RemoveWhere(x => (byte)x > 135);
|
|
|
|
Zones = Main.LIMap ? ShipStatus.Instance.AllRooms.Select(x => x.RoomId).TakeRandom(NumZones.GetInt()) : DefaultZones[Main.CurrentMap][NumZones.GetInt() - 1];
|
|
|
|
Points = AllKOTZTeam.ToDictionary(x => x, _ => 0);
|
|
PlayerPoints = Main.PlayerStates.Keys.ToDictionary(x => x, _ => 0);
|
|
ZoneDomination = Zones.ToDictionary(x => x, _ => KOTZTeam.None);
|
|
ZoneMoveSchedules = [];
|
|
ZoneDowntimeExpire = [];
|
|
RespawnTimes = [];
|
|
SpawnProtectionTimes = [];
|
|
TimeLeft = 0;
|
|
|
|
DefaultOutfits = Main.EnumeratePlayerControls().ToDictionary(x => x.PlayerId, x => x.Data.DefaultOutfit);
|
|
|
|
GameGoing = false;
|
|
SendRPC();
|
|
}
|
|
|
|
public static IEnumerator GameStart()
|
|
{
|
|
if (Options.CurrentGameMode != CustomGameMode.KingOfTheZones) yield break;
|
|
|
|
yield return new WaitForSecondsRealtime(3f);
|
|
|
|
var aapc = Main.AllAlivePlayerControlsToList;
|
|
int numPlayers = aapc.Count;
|
|
bool showTutorial = aapc.ExceptBy(PlayedFCs, x => x.FriendCode).Count() > numPlayers / 2;
|
|
NameNotifyManager.Reset();
|
|
|
|
if (AutoSetNumTeams.GetBool() && numPlayers % NumTeams.GetInt() != 0)
|
|
{
|
|
if (numPlayers is 2 or 3 or 5 or 7) NumTeams.SetValue(numPlayers - 2);
|
|
else if (numPlayers <= 1 || Enumerable.Range(2, (int)Math.Sqrt(numPlayers) - 1).Any(x => numPlayers % x == 0))
|
|
{
|
|
List<int> divisors = Enumerable.Range(2, 6).Where(x => numPlayers % x == 0).ToList();
|
|
|
|
if (divisors.Count > 0)
|
|
{
|
|
int selectedTeamCount = PreferNumTeams.GetValue() == 0 ? divisors.Min() : divisors.Max();
|
|
NumTeams.SetValue(selectedTeamCount - 2);
|
|
Logger.Msg($"Auto set teams to {selectedTeamCount}", "KOTZ");
|
|
}
|
|
}
|
|
}
|
|
|
|
int teams = NumTeams.GetInt();
|
|
int zones = NumZones.GetInt();
|
|
|
|
FixedUpdatePatch.NumTeamsCache = teams + 1;
|
|
|
|
PlayerTeams = aapc
|
|
.Select(x => x.PlayerId)
|
|
.Partition(teams)
|
|
.Zip(AllKOTZTeam[1..], (players, team) => players.ToDictionary(x => x, _ => team))
|
|
.SelectMany(x => x)
|
|
.ToDictionary(x => x.Key, x => x.Value);
|
|
|
|
foreach ((byte id, KOTZTeam team) in PlayerTeams)
|
|
{
|
|
try
|
|
{
|
|
PlayerControl player = id.GetPlayer();
|
|
if (!player) continue;
|
|
string name = Main.AllPlayerNames[id];
|
|
|
|
var writer = CustomRpcSender.Create("KOTZ.GameStart.TeamAssignmentNotifies", SendOption.Reliable);
|
|
var hasData = false;
|
|
|
|
string notify = Utils.ColorString(team.GetColor(), GetString($"KOTZ.Notify.AssignedToTeam.{team}"));
|
|
|
|
if (showTutorial)
|
|
{
|
|
string tutorial = string.Format(GetString("KOTZ.Notify.Tutorial.Basics"), teams, zones);
|
|
notify = notify.Insert(0, tutorial + "\n\n");
|
|
}
|
|
|
|
hasData |= CustomRpcSenderExtensions.Notify(ref writer, player, $"<#ffffff>{notify}</color>", 100f);
|
|
Logger.Info($"{name} assigned to {team} team", "KOTZ");
|
|
|
|
if (!player.AmOwner)
|
|
{
|
|
try
|
|
{
|
|
int targetClientId = player.OwnerId;
|
|
PlayerTeams.DoIf(
|
|
x => x.Key != id && x.Value == team,
|
|
x => writer.RpcSetRole(x.Key.GetPlayer(), RoleTypes.Impostor, targetClientId, changeRoleMap: true));
|
|
|
|
hasData = true;
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
}
|
|
|
|
writer.SendMessage(!hasData);
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
|
|
yield return null;
|
|
}
|
|
|
|
var sender = CustomRpcSender.Create("KOTZ.GameStart.TeamAssignmentNotifies", SendOption.Reliable);
|
|
|
|
foreach ((byte id, KOTZTeam team) in PlayerTeams)
|
|
{
|
|
try
|
|
{
|
|
PlayerControl player = id.GetPlayer();
|
|
if (!player) continue;
|
|
|
|
byte colorId = team.GetColorId();
|
|
player.SetColor(colorId);
|
|
|
|
sender.AutoStartRpc(player.NetId, RpcCalls.SetColor)
|
|
.Write(player.Data.NetId)
|
|
.Write(colorId)
|
|
.EndRpc();
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
|
|
yield return null;
|
|
}
|
|
|
|
sender.SendMessage();
|
|
|
|
yield return new WaitForSecondsRealtime(showTutorial ? 8f : 2f);
|
|
NameNotifyManager.Reset();
|
|
if (!GameStates.InGame || !Main.IntroDestroyed) goto End;
|
|
|
|
int tagCooldown = TagCooldown.GetInt();
|
|
int respawnTime = RespawnTime.GetInt();
|
|
|
|
if (showTutorial)
|
|
{
|
|
int points = PointsToWin.GetInt();
|
|
int timeLimit = MaxGameLength.GetInt();
|
|
var mins = $"{timeLimit / 60:N0}";
|
|
var secs = $"{timeLimit % 60:N0}";
|
|
|
|
bool endByPoints = GameEndsByPoints.GetBool();
|
|
bool endByTime = GameEndsByTimeLimit.GetBool();
|
|
|
|
string gameEnd = (endByPoints, endByTime) switch
|
|
{
|
|
(true, true) => string.Format(GetString("KOTZ.Notify.Tutorial.GameEnd.PointsOrTime"), points, mins, secs),
|
|
(true, false) => string.Format(GetString("KOTZ.Notify.Tutorial.GameEnd.PointsOnly"), points),
|
|
(false, true) => string.Format(GetString("KOTZ.Notify.Tutorial.GameEnd.TimeOnly"), mins, secs),
|
|
_ => string.Empty
|
|
};
|
|
|
|
aapc.NotifyPlayers($"<#ffffff>{gameEnd}</color>", 100f);
|
|
yield return new WaitForSecondsRealtime(4f);
|
|
NameNotifyManager.Reset();
|
|
if (!GameStates.InGame || !Main.IntroDestroyed) goto End;
|
|
|
|
string tags = string.Format(GetString("KOTZ.Notify.Tutorial.Tagging"), tagCooldown, respawnTime);
|
|
|
|
aapc.NotifyPlayers($"<#ffffff>{tags}</color>", 100f);
|
|
yield return new WaitForSecondsRealtime(4f);
|
|
NameNotifyManager.Reset();
|
|
if (!GameStates.InGame || !Main.IntroDestroyed) goto End;
|
|
|
|
if (ZonesMove.GetBool())
|
|
{
|
|
string zoneMovement = string.Format(GetString(AllZonesMoveAtOnce.GetBool() ? "KOTZ.Notify.Tutorial.ZonesMoving.AllAtOnce" : "KOTZ.Notify.Tutorial.ZonesMoving.Separately"), ZoneMoveTime.GetInt());
|
|
|
|
aapc.NotifyPlayers($"<#ffffff>{zoneMovement}</color>", 100f);
|
|
yield return new WaitForSecondsRealtime(4f);
|
|
NameNotifyManager.Reset();
|
|
if (!GameStates.InGame || !Main.IntroDestroyed) goto End;
|
|
|
|
int downTime = DowntimeAfterZoneMove.GetInt();
|
|
|
|
if (downTime > 0)
|
|
{
|
|
string downTimeInfo = string.Format(GetString("KOTZ.Notify.Tutorial.ZonesMoving.Downtime"), downTime);
|
|
|
|
aapc.NotifyPlayers($"<#ffffff>{downTimeInfo}</color>", 100f);
|
|
yield return new WaitForSecondsRealtime(4f);
|
|
NameNotifyManager.Reset();
|
|
if (!GameStates.InGame || !Main.IntroDestroyed) goto End;
|
|
}
|
|
}
|
|
|
|
yield return StartingCountdown();
|
|
}
|
|
else
|
|
{
|
|
string info = string.Format(GetString("KOTZ.Notify.ShortIntro"), teams, zones, tagCooldown, respawnTime);
|
|
|
|
if (LastShortInfo != info)
|
|
{
|
|
LastShortInfo = info;
|
|
aapc.NotifyPlayers($"<#ffffff>{info}</color>", 100f);
|
|
yield return new WaitForSecondsRealtime(4f);
|
|
NameNotifyManager.Reset();
|
|
if (!GameStates.InGame || !Main.IntroDestroyed) goto End;
|
|
}
|
|
else
|
|
yield return StartingCountdown();
|
|
}
|
|
|
|
if (!Main.LIMap)
|
|
{
|
|
KeyValuePair<SystemTypes, Vector2>[] spawnsConst = RandomSpawn.SpawnMap.GetSpawnMap().Positions.ExceptBy(Zones, x => x.Key).ToArray();
|
|
List<KeyValuePair<SystemTypes, Vector2>> spawns = spawnsConst.ToList();
|
|
|
|
foreach (PlayerControl player in aapc)
|
|
{
|
|
try
|
|
{
|
|
player.SetKillCooldown(GetKillCooldown(player));
|
|
|
|
KeyValuePair<SystemTypes, Vector2> spawn = spawns.RandomElement();
|
|
player.TP(spawn.Value);
|
|
spawns.RemoveAll(x => x.Key == spawn.Key);
|
|
|
|
if (spawns.Count == 0) spawns = spawnsConst.ToList();
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
|
|
yield return null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (PlayerControl player in aapc)
|
|
{
|
|
try
|
|
{
|
|
player.SetKillCooldown(GetKillCooldown(player));
|
|
player.TPToRandomVent();
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
TimeLeft = GameEndsByTimeLimit.GetBool() ? MaxGameLength.GetInt() : 0;
|
|
GameStartTS = Utils.TimeStamp;
|
|
|
|
GameGoing = true;
|
|
|
|
End:
|
|
|
|
yield return Utils.NotifyEveryoneAsync(false);
|
|
yield break;
|
|
|
|
IEnumerator StartingCountdown()
|
|
{
|
|
for (var i = 3; i > 0; i--)
|
|
{
|
|
NameNotifyManager.Reset();
|
|
aapc.NotifyPlayers($"<#ffffff>{string.Format(GetString("RR_ReadyQM"), i)}</color>", 100f);
|
|
yield return new WaitForSecondsRealtime(1f);
|
|
}
|
|
|
|
NameNotifyManager.Reset();
|
|
}
|
|
}
|
|
|
|
public static bool GetNameColor(PlayerControl pc, ref string color)
|
|
{
|
|
if (!GameGoing || !Main.IntroDestroyed) return false;
|
|
|
|
if (PlayerTeams.TryGetValue(pc.PlayerId, out KOTZTeam team))
|
|
{
|
|
color = team switch
|
|
{
|
|
KOTZTeam.Red => "#ff0000",
|
|
KOTZTeam.Yellow => "#ffff00",
|
|
KOTZTeam.Blue => "#00ffff",
|
|
KOTZTeam.Green => "#00ff00",
|
|
KOTZTeam.Tan => "#A88E8E",
|
|
KOTZTeam.Rose => "#FAB8EB",
|
|
KOTZTeam.Orange => "#ff8800",
|
|
_ => "#ffffff"
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static void OnCheckMurder(PlayerControl killer, PlayerControl target)
|
|
{
|
|
if (!Main.IntroDestroyed || !GameGoing || PlayerTeams[killer.PlayerId] == PlayerTeams[target.PlayerId] || SpawnProtectionTimes.ContainsKey(target.PlayerId) || new[] { killer, target }.Any(x => RespawnTimes.ContainsKey(x.PlayerId))) return;
|
|
|
|
killer.SetKillCooldown(GetKillCooldown(killer));
|
|
target.SetKillCooldown(GetKillCooldown(target));
|
|
|
|
RespawnTimes[target.PlayerId] = Utils.TimeStamp + RespawnTime.GetInt() + 1;
|
|
target.ExileTemporarily();
|
|
}
|
|
|
|
public static float GetKillCooldown(PlayerControl player)
|
|
{
|
|
float cd = TagCooldown.GetInt();
|
|
if (!PlayerTeams.TryGetValue(player.PlayerId, out KOTZTeam playerTeam)) return cd;
|
|
|
|
if (ZoneDomination.ContainsValue(playerTeam))
|
|
cd *= 1.5f;
|
|
|
|
Dictionary<KOTZTeam, byte[]> teamPlayers = PlayerTeams.GroupBy(x => x.Value).ToDictionary(x => x.Key, x => x.Select(g => g.Key).Where(g => g.GetPlayer()).ToArray());
|
|
int maxTeamSize = teamPlayers.Values.Max(x => x.Length);
|
|
int teamSize = teamPlayers[playerTeam].Length;
|
|
|
|
if (maxTeamSize > teamSize)
|
|
cd /= 2f;
|
|
|
|
return cd;
|
|
}
|
|
|
|
public static string GetSuffix(PlayerControl seer)
|
|
{
|
|
if (!Main.IntroDestroyed || !GameGoing) return string.Empty;
|
|
|
|
Suffix.Clear().Append("<#ffffff>");
|
|
long now = Utils.TimeStamp;
|
|
bool justStarted = GameStartTS + 15 > now;
|
|
|
|
if (RespawnTimes.TryGetValue(seer.PlayerId, out long respawnTS))
|
|
Suffix.AppendFormat(GetString("KOTZ.Suffix.RespawnTime"), respawnTS - now).AppendLine();
|
|
else if (SpawnProtectionTimes.TryGetValue(seer.PlayerId, out long protectionEndTS))
|
|
Suffix.AppendFormat(GetString("KOTZ.Suffix.SpawnProtectionTime"), protectionEndTS - now).AppendLine();
|
|
|
|
PlainShipRoom room = seer.GetPlainShipRoom();
|
|
SystemTypes zone = !room ? SystemTypes.Hallway : room.RoomId;
|
|
|
|
if (justStarted) Suffix.Append(GetString(Zones.Count == 1 ? "KOTZ.SuffixHelp.Zones.Single" : "KOTZ.SuffixHelp.Zones.Plural"));
|
|
Suffix.AppendLine(Zones.Join(" | ", x => Utils.ColorString(ZoneDomination[x].GetColor(), (zone == x ? "<u>" : string.Empty) + GetString($"{x}") + (zone == x ? "</u>" : string.Empty) + (ZoneMoveSchedules.TryGetValue(x, out long moveTS) ? $"<size=80%> {(ZoneDowntimeExpire.TryGetValue(x, out long downtimeEndTS) ? Utils.ColorString(Color.gray, $"{downtimeEndTS - now}") : $"<#ffff44>{moveTS - now}</color>")}</size>" : string.Empty))));
|
|
|
|
if (justStarted) Suffix.Append(GetString("KOTZ.SuffixHelp.Points"));
|
|
Suffix.AppendLine(Points.IntersectBy(PlayerTeams.Values, x => x.Key).Join(" | ", x => Utils.ColorString(x.Key.GetColor(), $"{x.Value}")));
|
|
|
|
int highestPoints = Points.Values.Max();
|
|
bool tie = Points.Values.Count(x => x == highestPoints) > 1;
|
|
bool end = GameEndsByTimeLimit.GetBool() && TimeLeft <= 0 || GameEndsByPoints.GetBool() && highestPoints >= PointsToWin.GetInt();
|
|
|
|
if (tie && end)
|
|
{
|
|
string tieTeams = Points.Where(x => x.Value == highestPoints).Join(' ', x => Utils.ColorString(x.Key.GetColor(), "\u25a0"));
|
|
Suffix.Append("<size=80%>").AppendFormat(GetString("KOTZ.Suffix.Tie"), tieTeams).AppendLine("</size>");
|
|
}
|
|
else
|
|
{
|
|
if (GameEndsByTimeLimit.GetBool())
|
|
{
|
|
if (justStarted) Suffix.Append(GetString("KOTZ.SuffixHelp.GameEndTimer"));
|
|
Suffix.AppendLine($"<size=80%>{TimeLeft / 60:N0}:{TimeLeft % 60:00}</size>");
|
|
}
|
|
|
|
if (GameEndsByPoints.GetBool())
|
|
Suffix.Append("<size=80%>").AppendFormat(GetString("KOTZ.Suffix.PointsToWin"), PointsToWin.GetInt()).AppendLine("</size>");
|
|
}
|
|
|
|
return Suffix.Append("</color>").ToString().Trim();
|
|
}
|
|
|
|
public static string GetStatistics(byte id)
|
|
{
|
|
return string.Format(GetString("KOTZ.EndScreen.Statistics"), GetZoneTime(id));
|
|
}
|
|
|
|
public static int GetZoneTime(byte id)
|
|
{
|
|
return PlayerPoints.GetValueOrDefault(id);
|
|
}
|
|
|
|
public static bool CheckForGameEnd(out GameOverReason reason)
|
|
{
|
|
reason = GameOverReason.ImpostorsByKill;
|
|
|
|
if (!Main.IntroDestroyed) return false;
|
|
|
|
PlayerControl[] aapc = Main.EnumerateAlivePlayerControls().Concat(ExtendedPlayerControl.TempExiled.ToValidPlayers()).ToArray();
|
|
|
|
switch (aapc.Length)
|
|
{
|
|
case 0:
|
|
{
|
|
ResetSkins();
|
|
CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Draw);
|
|
CustomWinnerHolder.WinnerIds = Main.PlayerStates.Keys.ToHashSet();
|
|
reason = GameOverReason.CrewmateDisconnect;
|
|
return true;
|
|
}
|
|
case 1:
|
|
{
|
|
ResetSkins();
|
|
KOTZTeam winner = PlayerTeams[aapc[0].PlayerId];
|
|
Color color = winner.GetColor();
|
|
CustomWinnerHolder.WinnerIds = PlayerTeams.Where(x => x.Value == winner).Select(x => x.Key).ToHashSet();
|
|
WinnerData = (color, Utils.ColorString(color, GetString($"KOTZ.EndScreen.Winner.{winner}")));
|
|
SendRPC();
|
|
return true;
|
|
}
|
|
default:
|
|
{
|
|
if (Options.IntegrateNaturalDisasters.GetBool() && AllKOTZTeam.FindFirst(x => aapc.All(p => PlayerTeams[p.PlayerId] == x), out KOTZTeam team))
|
|
{
|
|
ResetSkins();
|
|
Color color = team.GetColor();
|
|
CustomWinnerHolder.WinnerIds = PlayerTeams.Where(x => x.Value == team).Select(x => x.Key).ToHashSet();
|
|
WinnerData = (color, Utils.ColorString(color, GetString($"KOTZ.EndScreen.Winner.{team}")));
|
|
SendRPC();
|
|
return true;
|
|
}
|
|
|
|
return WinnerData.Team != "No one wins";
|
|
}
|
|
}
|
|
|
|
void ResetSkins()
|
|
{
|
|
DefaultOutfits.Select(x => (pc: x.Key.GetPlayer(), outfit: x.Value)).DoIf(x => x.pc && x.outfit != null, x => Utils.RpcChangeSkin(x.pc, x.outfit));
|
|
DefaultOutfits = [];
|
|
}
|
|
}
|
|
|
|
public static bool IsNotInLocalPlayersTeam(PlayerControl pc)
|
|
{
|
|
return pc.IsValidTargetForKillButton() && (!PlayerTeams.TryGetValue(pc.PlayerId, out KOTZTeam team) || !PlayerTeams.TryGetValue(PlayerControl.LocalPlayer.PlayerId, out KOTZTeam lpTeam) || team != lpTeam);
|
|
}
|
|
|
|
private static void SendRPC()
|
|
{
|
|
var w = Utils.CreateRPC(CustomRPC.KOTZSync);
|
|
|
|
w.Write(WinnerData.Color);
|
|
w.Write(WinnerData.Team);
|
|
|
|
w.Write(Points.Count);
|
|
|
|
foreach ((KOTZTeam team, int points) in Points)
|
|
{
|
|
w.Write((int)team);
|
|
w.Write(points);
|
|
}
|
|
|
|
Utils.EndRPC(w);
|
|
}
|
|
|
|
public static void ReceiveRPC(MessageReader reader)
|
|
{
|
|
WinnerData.Color = reader.ReadColor();
|
|
WinnerData.Team = reader.ReadString();
|
|
|
|
Loop.Times(reader.ReadInt32(), _ => Points[(KOTZTeam)reader.ReadInt32()] = reader.ReadInt32());
|
|
}
|
|
|
|
private enum KOTZTeam
|
|
{
|
|
None,
|
|
Red,
|
|
Yellow,
|
|
Blue,
|
|
Green,
|
|
Tan,
|
|
Rose,
|
|
Orange
|
|
}
|
|
|
|
//[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))]
|
|
public static class FixedUpdatePatch
|
|
{
|
|
private static long LastUpdate;
|
|
public static int NumTeamsCache = Enum.GetNames(typeof(KOTZTeam)).Length;
|
|
|
|
public static void Postfix()
|
|
{
|
|
if (!AmongUsClient.Instance.AmHost || !GameStates.IsInTask || ExileController.Instance || Options.CurrentGameMode != CustomGameMode.KingOfTheZones || !Main.IntroDestroyed || !GameGoing) return;
|
|
|
|
long now = Utils.TimeStamp;
|
|
if (LastUpdate == now) return;
|
|
LastUpdate = now;
|
|
|
|
Main.Instance.StartCoroutine(Coroutine());
|
|
return;
|
|
|
|
IEnumerator Coroutine()
|
|
{
|
|
try
|
|
{
|
|
if (ZonesMove.GetBool())
|
|
{
|
|
int moveTime = ZoneMoveTime.GetInt();
|
|
double timePart = moveTime / NumZones.GetFloat();
|
|
bool atOnce = AllZonesMoveAtOnce.GetBool();
|
|
int downtime = DowntimeAfterZoneMove.GetInt();
|
|
|
|
if (ZoneMoveSchedules.Count == 0)
|
|
{
|
|
for (int index = 0; index < Zones.Count; index++)
|
|
{
|
|
SystemTypes zone = Zones[index];
|
|
int timeFromNow = atOnce ? moveTime : (int)Math.Ceiling(timePart * (index + 1));
|
|
ZoneMoveSchedules[zone] = now + timeFromNow;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (atOnce)
|
|
{
|
|
if (Zones.Count > 0 && ZoneMoveSchedules.TryGetValue(Zones[0], out long moveTS) && moveTS <= now)
|
|
{
|
|
List<SystemTypes> newZones = AllRooms.Except(Zones).TakeRandom(Zones.Count);
|
|
|
|
Zones.Clear();
|
|
ZoneDomination.Clear();
|
|
|
|
for (var index = 0; index < newZones.Count; index++)
|
|
{
|
|
SystemTypes newZone = newZones[index];
|
|
Zones.Add(newZone);
|
|
ZoneDomination[newZone] = KOTZTeam.None;
|
|
ZoneMoveSchedules[newZone] = now + moveTime + downtime;
|
|
if (downtime > 0) ZoneDowntimeExpire[newZone] = now + downtime;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SystemTypes? added = null;
|
|
SystemTypes? removed = null;
|
|
|
|
foreach ((SystemTypes zone, long moveTS) in ZoneMoveSchedules)
|
|
{
|
|
if (moveTS > now) continue;
|
|
|
|
try
|
|
{
|
|
SystemTypes newZone = AllRooms.Except(Zones).RandomElement();
|
|
|
|
int index = Zones.IndexOf(zone);
|
|
Zones.RemoveAt(index);
|
|
Zones.Insert(index, newZone);
|
|
|
|
added = newZone;
|
|
removed = zone;
|
|
ZoneDomination.Remove(zone);
|
|
|
|
Logger.Info($"Zone moved: {zone} => {newZone}", "KOTZ");
|
|
break;
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
}
|
|
|
|
if (removed.HasValue) ZoneMoveSchedules.Remove(removed.Value);
|
|
|
|
if (added.HasValue)
|
|
{
|
|
ZoneDomination[added.Value] = KOTZTeam.None;
|
|
ZoneMoveSchedules[added.Value] = now + moveTime + downtime;
|
|
if (downtime > 0) ZoneDowntimeExpire[added.Value] = now + downtime;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (downtime > 0 && ZoneDowntimeExpire.Count > 0)
|
|
{
|
|
if (atOnce)
|
|
{
|
|
if (Zones.Count > 0 && ZoneDowntimeExpire.TryGetValue(Zones[0], out long expireTS) && expireTS <= now)
|
|
ZoneDowntimeExpire.Clear();
|
|
}
|
|
else
|
|
{
|
|
SystemTypes? toRemove = null;
|
|
|
|
foreach ((SystemTypes zone, long downtimeExpire) in ZoneDowntimeExpire)
|
|
{
|
|
if (downtimeExpire > now) continue;
|
|
toRemove = zone;
|
|
break;
|
|
}
|
|
|
|
if (toRemove.HasValue) ZoneDowntimeExpire.Remove(toRemove.Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
|
|
yield return null;
|
|
|
|
try
|
|
{
|
|
if (SpawnProtectionTimes.Count > 0)
|
|
{
|
|
List<byte> toRemove = null;
|
|
|
|
foreach ((byte id, long protectionTS) in SpawnProtectionTimes)
|
|
{
|
|
if (protectionTS > now) continue;
|
|
toRemove ??= [];
|
|
toRemove.Add(id);
|
|
}
|
|
|
|
toRemove?.ForEach(x => SpawnProtectionTimes.Remove(x));
|
|
}
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
|
|
yield return null;
|
|
|
|
try
|
|
{
|
|
if (RespawnTimes.Count > 0)
|
|
{
|
|
List<byte> toRemove = null;
|
|
|
|
foreach ((byte id, long respawnTS) in RespawnTimes)
|
|
{
|
|
if (respawnTS > now) continue;
|
|
|
|
try
|
|
{
|
|
PlayerControl player = id.GetPlayer();
|
|
if (!player) continue;
|
|
|
|
player.ReviveFromTemporaryExile();
|
|
|
|
if (!Main.LIMap) player.TP(RandomSpawn.SpawnMap.GetSpawnMap().Positions.ExceptBy(Zones, x => x.Key).RandomElement().Value);
|
|
else player.TPToRandomVent();
|
|
|
|
RPC.PlaySoundRPC(player.PlayerId, Sounds.SpawnSound);
|
|
Utils.NotifyRoles(SpecifyTarget: player, SendOption: SendOption.None);
|
|
|
|
int spawnProtectionTime = SpawnProtectionTime.GetInt();
|
|
if (spawnProtectionTime > 0) SpawnProtectionTimes[id] = now + spawnProtectionTime;
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
|
|
toRemove ??= [];
|
|
toRemove.Add(id);
|
|
|
|
Logger.Info($"{Main.AllPlayerNames[id]} respawned", "KOTZ");
|
|
}
|
|
|
|
toRemove?.ForEach(x => RespawnTimes.Remove(x));
|
|
}
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
|
|
yield return null;
|
|
|
|
try
|
|
{
|
|
Dictionary<SystemTypes, (int[] TeamCounts, List<(byte Id, int Team)> Players)> zoneInfo = new(Zones.Count);
|
|
|
|
foreach (SystemTypes zone in Zones)
|
|
zoneInfo[zone] = (new int[NumTeamsCache], []);
|
|
|
|
foreach (PlayerControl player in Main.EnumerateAlivePlayerControls())
|
|
{
|
|
if (RespawnTimes.ContainsKey(player.PlayerId))
|
|
continue;
|
|
|
|
int team = (int)PlayerTeams[player.PlayerId];
|
|
|
|
foreach (SystemTypes zone in Zones)
|
|
{
|
|
if (player.IsInRoom(zone))
|
|
{
|
|
(int[] TeamCounts, List<(byte Id, int Team)> Players) info = zoneInfo[zone];
|
|
info.TeamCounts[team]++;
|
|
info.Players.Add((player.PlayerId, team));
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (SystemTypes zone in Zones)
|
|
{
|
|
if (ZoneDowntimeExpire.ContainsKey(zone))
|
|
{
|
|
ZoneDomination[zone] = KOTZTeam.None;
|
|
continue;
|
|
}
|
|
|
|
int[] counts = zoneInfo[zone].TeamCounts;
|
|
|
|
int maxTeam = -1;
|
|
int max = 0;
|
|
int second = 0;
|
|
|
|
for (int i = 0; i < counts.Length; i++)
|
|
{
|
|
int c = counts[i];
|
|
|
|
if (c > max)
|
|
{
|
|
second = max;
|
|
max = c;
|
|
maxTeam = i;
|
|
}
|
|
else if (c > second)
|
|
{
|
|
second = c;
|
|
}
|
|
}
|
|
|
|
ZoneDomination[zone] =
|
|
max == 0 || max == second
|
|
? KOTZTeam.None
|
|
: (KOTZTeam)maxTeam;
|
|
}
|
|
|
|
foreach ((SystemTypes zone, KOTZTeam team) in ZoneDomination)
|
|
{
|
|
if (team != KOTZTeam.None)
|
|
{
|
|
Points[team]++;
|
|
zoneInfo[zone].Players.DoIf(x => x.Team == (int)team, x => PlayerPoints[x.Id]++);
|
|
}
|
|
}
|
|
|
|
Logger.Info($"Zone domination: {ZoneDomination.Join(", ", x => $"{x.Key} = {x.Value}")}", "KOTZ");
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
|
|
yield return null;
|
|
|
|
foreach (PlayerControl player in Main.EnumerateAlivePlayerControls())
|
|
{
|
|
try
|
|
{
|
|
player.RpcChangeColor(PlayerTeams[player.PlayerId].GetColorId());
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
}
|
|
|
|
yield return null;
|
|
|
|
int highestPoints = Points.Values.Max();
|
|
bool tie = Points.Values.Count(x => x == highestPoints) > 1;
|
|
|
|
if (GameEndsByTimeLimit.GetBool())
|
|
{
|
|
if (TimeLeft > 0) TimeLeft--;
|
|
if (TimeLeft <= 0) EndGame();
|
|
}
|
|
|
|
if (GameEndsByPoints.GetBool() && highestPoints >= PointsToWin.GetInt())
|
|
EndGame();
|
|
|
|
yield return null;
|
|
|
|
Utils.NotifyRoles();
|
|
yield break;
|
|
|
|
void EndGame()
|
|
{
|
|
if (tie) return;
|
|
GameGoing = false;
|
|
DefaultOutfits.Select(x => (pc: x.Key.GetPlayer(), outfit: x.Value)).DoIf(x => x.pc && x.outfit != null, x => Utils.RpcChangeSkin(x.pc, x.outfit));
|
|
KOTZTeam winner = Points.GetKeyByValue(highestPoints);
|
|
CustomWinnerHolder.WinnerIds = PlayerTeams.Where(x => x.Value == winner).Select(x => x.Key).ToHashSet();
|
|
Color color = winner.GetColor();
|
|
WinnerData = (color, Utils.ColorString(color, GetString($"KOTZ.EndScreen.Winner.{winner}")));
|
|
SendRPC();
|
|
|
|
Logger.Info($"Game ended. Winner: {winner} team", "KOTZ");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|