diff --git a/CustomRoles.cs b/CustomRoles.cs index 3a853d42b..ec4ca0f29 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -280,6 +280,7 @@ public enum CustomRoles Samurai, SchrodingersCat, NSerialKiller, // Serial Killer + Shifter, Sidekick, Simon, SoulHunter, diff --git a/GameMode/HnSManager.cs b/GameMode/HnSManager.cs index 456f21ca2..01b9c0acd 100644 --- a/GameMode/HnSManager.cs +++ b/GameMode/HnSManager.cs @@ -122,7 +122,7 @@ namespace EHR }; memberNum[Team.Crewmate] = allPlayers.Count - memberNum.Values.Sum(); - Logger.Warn($"Number of impostors: {memberNum[Team.Impostor]}", "debug"); + Logger.Warn($"Number of impostors: {memberNum[Team.Impostor]}", "HnsRoleAssigner"); foreach (var item in Main.SetRoles) { @@ -136,7 +136,7 @@ namespace EHR role.Value[item.Value]--; memberNum[role.Key]--; - Logger.Warn($"Pre-Set Role Assigned: {pc.GetRealName()} => {item.Value}", "CustomRoleSelector"); + Logger.Warn($"Pre-Set Role Assigned: {pc.GetRealName()} => {item.Value}", "HnsRoleAssigner"); } var playerTeams = Enum.GetValues()[1..] @@ -186,14 +186,14 @@ namespace EHR foreach (PlayerControl pc in allPlayers.Except(result.Keys).ToArray()) { - Logger.Warn($"Unassigned, force Hider: {pc.GetRealName()} => {CustomRoles.Hider}", "debug"); + 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())}", "CustomRoleSelector"); - Logger.Msg($"Roles: {result.Join(x => $"{x.Key.GetRealName()} => {x.Value}")}", "HideAndSeekRoleSelector"); + 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"); var roleInterfaces = Assembly.GetExecutingAssembly().GetTypes() .Where(x => typeof(IHideAndSeekRole).IsAssignableFrom(x) && !x.IsInterface) @@ -203,6 +203,14 @@ namespace EHR PlayerRoles = result.ToDictionary(x => x.Key.PlayerId, x => (roleInterfaces[x.Value.ToString()], x.Value)); result.IntersectBy(Main.PlayerStates.Keys, x => x.Key.PlayerId).Do(x => x.Key.RpcSetCustomRole(x.Value)); + + // ================================================================================================================== + + if (result.ContainsValue(CustomRoles.Agent)) + { + var agent = result.GetKeyByValue(CustomRoles.Agent).PlayerId; + PlayerRoles.DoIf(x => x.Value.Interface.Team == Team.Impostor, x => TargetArrow.Add(x.Key, agent)); + } } public static void ApplyGameOptions(IGameOptions opt, PlayerControl pc) @@ -248,6 +256,12 @@ namespace EHR string dangerMeter = GetDangerMeter(seer); + if (PlayerRoles[seer.PlayerId].Interface.Team == Team.Impostor && PlayerRoles.Values.Any(x => x.Role == CustomRoles.Agent)) + { + var agent = PlayerRoles.First(x => x.Value.Role == CustomRoles.Agent).Key; + dangerMeter += TargetArrow.GetArrows(seer, agent); + } + if (!isHUD && seer.IsModClient()) return string.Empty; if (TimeLeft <= 60) { @@ -304,9 +318,7 @@ namespace EHR // If there are no crew roles left, the game is over and only impostors win if (alivePlayers.All(x => PlayerRoles[x.PlayerId].Interface.Team != Team.Crewmate)) { - CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Seeker); - CustomWinnerHolder.WinnerIds.UnionWith(PlayerRoles.Where(x => x.Value.Interface.Team == Team.Impostor).Select(x => x.Key)); - AddFoxesToWinners(); + SetWinners(CustomWinner.Seeker, Team.Impostor); return true; } @@ -314,13 +326,18 @@ namespace EHR if (TimeLeft <= 0) { reason = GameOverReason.HumansByTask; - CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Hider); - CustomWinnerHolder.WinnerIds.UnionWith(PlayerRoles.Where(x => x.Value.Interface.Team == Team.Crewmate).Select(x => x.Key)); - AddFoxesToWinners(); + 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() diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 0d2fa41e6..f319e5629 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -362,6 +362,7 @@ internal static class CustomRolesHelper CustomRoles.PlagueDoctor => RoleTypes.Impostor, CustomRoles.Postman => RoleTypes.Impostor, CustomRoles.SchrodingersCat => RoleTypes.Impostor, + CustomRoles.Shifter => RoleTypes.Impostor, CustomRoles.Impartial => RoleTypes.Impostor, CustomRoles.Predator => RoleTypes.Impostor, CustomRoles.Reckless => RoleTypes.Impostor, @@ -400,6 +401,7 @@ internal static class CustomRolesHelper public static bool IsNonNK(this CustomRoles role, bool check = false) => (!check && role == CustomRoles.Arsonist && CanCheck && Options.IsLoaded && Options.ArsonistCanIgniteAnytime != null && !Options.ArsonistCanIgniteAnytime.GetBool()) || role is CustomRoles.Jester or CustomRoles.Postman or + CustomRoles.Shifter or CustomRoles.SchrodingersCat or CustomRoles.Impartial or CustomRoles.Predator or @@ -515,6 +517,7 @@ internal static class CustomRolesHelper public static bool IsNC(this CustomRoles role) => role is CustomRoles.Mario or + CustomRoles.Shifter or CustomRoles.Terrorist or CustomRoles.Revolutionist or CustomRoles.Impartial or @@ -1021,9 +1024,10 @@ internal static class CustomRolesHelper CustomRoles.Parasite => CountTypes.Impostor, CustomRoles.Crewpostor => CountTypes.Impostor, CustomRoles.Refugee => CountTypes.Impostor, - CustomRoles.DarkHide => DarkHide.SnatchesWin.GetBool() ? CountTypes.Crew : CountTypes.DarkHide, - CustomRoles.Arsonist => !Options.ArsonistKeepsGameGoing.GetBool() ? CountTypes.Crew : CountTypes.Arsonist, + CustomRoles.DarkHide when DarkHide.SnatchesWin.GetBool() => CountTypes.Crew, + CustomRoles.Arsonist when !Options.ArsonistKeepsGameGoing.GetBool() => CountTypes.Crew, CustomRoles.SchrodingersCat => SchrodingersCat.WinsWithCrewIfNotAttacked.GetBool() ? CountTypes.Crew : CountTypes.OutOfGame, + CustomRoles.Shifter => CountTypes.OutOfGame, _ => Enum.TryParse(role.ToString(), true, out CountTypes type) ? type diff --git a/Modules/CustomTeamManager.cs b/Modules/CustomTeamManager.cs index 06c37c243..8af056b4b 100644 --- a/Modules/CustomTeamManager.cs +++ b/Modules/CustomTeamManager.cs @@ -76,7 +76,7 @@ namespace EHR.Modules foreach ((CustomTeam team, HashSet players) in CustomTeamPlayerIds) { - if (!GetSettingForTeam(team, "Arrows")) continue; + if (!IsSettingEnabledForTeam(team, "Arrows")) continue; foreach (byte player in players) { @@ -91,7 +91,7 @@ namespace EHR.Modules public static string GetSuffix(PlayerControl seer) { - if (seer == null || EnabledCustomTeams.Count == 0 || !GetSettingForPlayerTeam(seer.PlayerId, "Arrows")) return string.Empty; + if (seer == null || EnabledCustomTeams.Count == 0 || !IsSettingEnabledForPlayerTeam(seer.PlayerId, "Arrows")) return string.Empty; return CustomTeamPlayerIds[GetCustomTeam(seer.PlayerId)].Aggregate(string.Empty, (s, id) => s + Utils.ColorString(Main.PlayerColors.GetValueOrDefault(id, Color.white), TargetArrow.GetArrows(seer, id))); } @@ -143,13 +143,13 @@ namespace EHR.Modules return team1 != null && team2 != null && team1.Equals(team2); } - public static bool GetSettingForPlayerTeam(byte id, string settingName) + public static bool IsSettingEnabledForPlayerTeam(byte id, string settingName) { var team = GetCustomTeam(id); - return team != null && GetSettingForTeam(team, settingName); + return team != null && IsSettingEnabledForTeam(team, settingName); } - public static bool GetSettingForTeam(CustomTeam team, string settingName) + public static bool IsSettingEnabledForTeam(CustomTeam team, string settingName) { var optionsGroup = CustomTeamOptions.First(x => x.Team.Equals(team)); var setting = optionsGroup.GetType().GetFields().FirstOrDefault(x => x.Name.Contains(settingName)); diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index cc1e9edbb..eaaecfee4 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -24,7 +24,7 @@ static class ExtendedPlayerControl { public const MurderResultFlags ResultFlags = MurderResultFlags.Succeeded; - public static void RpcSetCustomRole(this PlayerControl player, CustomRoles role, bool isRoleForced = false) + public static void RpcSetCustomRole(this PlayerControl player, CustomRoles role, bool isRoleForced = false, bool replaceAllAddons = false) { if (role < CustomRoles.NotAssigned || isRoleForced) { @@ -33,7 +33,7 @@ static class ExtendedPlayerControl else { if (!Cleanser.CleansedCanGetAddon.GetBool() && player.Is(CustomRoles.Cleansed)) return; - Main.PlayerStates[player.PlayerId].SetSubRole(role); + Main.PlayerStates[player.PlayerId].SetSubRole(role, replaceAllAddons); } if (AmongUsClient.Instance.AmHost) @@ -41,6 +41,7 @@ static class ExtendedPlayerControl MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetCustomRole, SendOption.Reliable); writer.Write(player.PlayerId); writer.WritePacked((int)role); + writer.Write(replaceAllAddons); AmongUsClient.Instance.FinishRpcImmediately(writer); } } @@ -869,6 +870,8 @@ static class ExtendedPlayerControl if (Options.CurrentGameMode == CustomGameMode.SoloKombat) return; if (target == null) target = killer; + CheckAndSpawnAdditionalRefugee(target.Data); + if (target.GetTeam() is Team.Impostor or Team.Neutral) Stressed.OnNonCrewmateDead(); if (killer.Is(CustomRoles.Damocles)) Damocles.OnMurder(killer.PlayerId); diff --git a/Modules/GameState.cs b/Modules/GameState.cs index 57a87739e..fbe595467 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -132,7 +132,11 @@ public class PlayerState(byte playerId) { if (role == CustomRoles.Cleansed) AllReplace = true; - if (AllReplace) SubRoles.Clear(); + if (AllReplace) + { + SubRoles.Clear(); + Utils.SendRPC(CustomRPC.RemoveSubRole, PlayerId, 2); + } if (!SubRoles.Contains(role)) SubRoles.Add(role); @@ -229,7 +233,7 @@ public class PlayerState(byte playerId) if (SubRoles.Contains(role)) SubRoles.Remove(role); - Utils.SendRPC(CustomRPC.RemoveSubRole, PlayerId, (int)role); + Utils.SendRPC(CustomRPC.RemoveSubRole, PlayerId, 1, (int)role); } public void SetDead() diff --git a/Modules/GhostRolesManager.cs b/Modules/GhostRolesManager.cs index 289138c42..4229222dd 100644 --- a/Modules/GhostRolesManager.cs +++ b/Modules/GhostRolesManager.cs @@ -35,6 +35,8 @@ namespace EHR.Modules IGhostRole instance = CreateGhostRoleInstance(suitableRole); instance.OnAssign(pc); AssignedGhostRoles[pc.PlayerId] = (suitableRole, instance); + + if (suitableRole == CustomRoles.Haunter) GhostRoles.Remove(suitableRole); } public static void SpecificAssignGhostRole(byte id, CustomRoles role, bool set) @@ -57,16 +59,11 @@ namespace EHR.Modules if (pc.IsAlive() || pc.GetCountTypes() is CountTypes.None or CountTypes.OutOfGame || pc.Is(CustomRoles.EvilSpirit)) return false; var suitableRole = GetSuitableGhostRole(pc); - switch (suitableRole) + return suitableRole switch { - case CustomRoles.Specter when IsPartnerPickedRole(): - return false; - case CustomRoles.Haunter: - GhostRoles.Remove(CustomRoles.Haunter); - break; - } - - return suitableRole.IsGhostRole() && !AssignedGhostRoles.Any(x => x.Key == pc.PlayerId || x.Value.Role == suitableRole); + CustomRoles.Specter when IsPartnerPickedRole() => false, + _ => suitableRole.IsGhostRole() && !AssignedGhostRoles.Any(x => x.Key == pc.PlayerId || x.Value.Role == suitableRole) + }; bool IsPartnerPickedRole() => Main.PlayerStates[pc.PlayerId].Role switch { diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs index 6e4ca05ac..d7f908a27 100644 --- a/Modules/GuessManager.cs +++ b/Modules/GuessManager.cs @@ -182,7 +182,7 @@ public static class GuessManager return true; } - if (CustomTeamManager.AreInSameCustomTeam(pc.PlayerId, targetId) && !CustomTeamManager.GetSettingForPlayerTeam(targetId, "GuessEachOther")) + if (CustomTeamManager.AreInSameCustomTeam(pc.PlayerId, targetId) && !CustomTeamManager.IsSettingEnabledForPlayerTeam(targetId, "GuessEachOther")) { if (!isUI) Utils.SendMessage(GetString("GuessSameCTAPlayer"), pc.PlayerId); else pc.ShowPopUp(GetString("GuessSameCTAPlayer")); diff --git a/Modules/NameColorManager.cs b/Modules/NameColorManager.cs index 6dbd6f654..12968c748 100644 --- a/Modules/NameColorManager.cs +++ b/Modules/NameColorManager.cs @@ -71,7 +71,7 @@ public static class NameColorManager } // Custom Teams - if (CustomTeamManager.AreInSameCustomTeam(seer.PlayerId, target.PlayerId) && CustomTeamManager.GetSettingForPlayerTeam(seer.PlayerId, "KnowRoles")) color = Main.RoleColors[target.GetCustomRole()]; + if (CustomTeamManager.AreInSameCustomTeam(seer.PlayerId, target.PlayerId) && CustomTeamManager.IsSettingEnabledForPlayerTeam(seer.PlayerId, "KnowRoles")) color = Main.RoleColors[target.GetCustomRole()]; // Add-ons if (target.Is(CustomRoles.Glow) && Utils.IsActive(SystemTypes.Electrical)) color = Main.RoleColors[CustomRoles.Glow]; diff --git a/Modules/RPC.cs b/Modules/RPC.cs index a18ac9c11..617d51666 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -51,6 +51,7 @@ public enum CustomRPC ShowPopUp, KillFlash, SyncAbilityUseLimit, + RemoveAbilityUseLimit, RemoveSubRole, Arrow, @@ -338,15 +339,23 @@ internal class RPCHandlerPatch case CustomRPC.SetCustomRole: byte CustomRoleTargetId = reader.ReadByte(); CustomRoles role = (CustomRoles)reader.ReadPackedInt32(); - RPC.SetCustomRole(CustomRoleTargetId, role); + bool replaceAllAddons = reader.ReadBoolean(); + RPC.SetCustomRole(CustomRoleTargetId, role, replaceAllAddons); break; case CustomRPC.SyncAbilityUseLimit: var pc = Utils.GetPlayerById(reader.ReadByte()); pc.SetAbilityUseLimit(reader.ReadSingle(), rpc: false); break; - case CustomRPC.RemoveSubRole: - Main.PlayerStates[reader.ReadByte()].RemoveSubRole((CustomRoles)reader.ReadPackedInt32()); + case CustomRPC.RemoveAbilityUseLimit: + Main.AbilityUseLimit.Remove(reader.ReadByte()); break; + case CustomRPC.RemoveSubRole: + { + byte id = reader.ReadByte(); + if (reader.ReadPackedInt32() == 2) Main.PlayerStates[id].SubRoles.Clear(); + else Main.PlayerStates[id].RemoveSubRole((CustomRoles)reader.ReadPackedInt32()); + break; + } case CustomRPC.Arrow: if (reader.ReadBoolean()) TargetArrow.ReceiveRPC(reader); else LocateArrow.ReceiveRPC(reader); @@ -989,7 +998,7 @@ internal static class RPC } } - public static void SetCustomRole(byte targetId, CustomRoles role) + public static void SetCustomRole(byte targetId, CustomRoles role, bool replaceAllAddons = false) { if (role < CustomRoles.NotAssigned) { @@ -997,7 +1006,7 @@ internal static class RPC } else { - Main.PlayerStates[targetId].SetSubRole(role); + Main.PlayerStates[targetId].SetSubRole(role, replaceAllAddons); } HudManager.Instance.SetHudActive(true); diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 03808bc88..8d8f6b82f 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -600,6 +600,7 @@ public static class Utils case CustomRoles.PlagueDoctor: case CustomRoles.Postman: case CustomRoles.SchrodingersCat: + case CustomRoles.Shifter: case CustomRoles.Impartial: case CustomRoles.Predator: case CustomRoles.Reckless: @@ -773,7 +774,7 @@ public static class Utils __instance.Is(CustomRoleTypes.Impostor) && PlayerControl.LocalPlayer.Is(CustomRoles.Crewpostor) && Options.AlliesKnowCrewpostor.GetBool() || __instance.Is(CustomRoleTypes.Impostor) && PlayerControl.LocalPlayer.Is(CustomRoleTypes.Impostor) && Options.ImpKnowAlliesRole.GetBool() || __instance.Is(CustomRoleTypes.Impostor) && PlayerControl.LocalPlayer.Is(CustomRoles.Madmate) && Options.MadmateKnowWhosImp.GetBool() || - CustomTeamManager.AreInSameCustomTeam(__instance.PlayerId, PlayerControl.LocalPlayer.PlayerId) && CustomTeamManager.GetSettingForPlayerTeam(__instance.PlayerId, "KnowRoles") || + CustomTeamManager.AreInSameCustomTeam(__instance.PlayerId, PlayerControl.LocalPlayer.PlayerId) && CustomTeamManager.IsSettingEnabledForPlayerTeam(__instance.PlayerId, "KnowRoles") || Main.PlayerStates.Values.Any(x => x.Role.KnowRole(PlayerControl.LocalPlayer, __instance)) || PlayerControl.LocalPlayer.IsRevealedPlayer(__instance) || PlayerControl.LocalPlayer.Is(CustomRoles.God) || @@ -1635,6 +1636,23 @@ public static class Utils } } + public static void CheckAndSpawnAdditionalRefugee(GameData.PlayerInfo deadPlayer) + { + if (deadPlayer != null && Options.SpawnAdditionalRefugeeOnImpsDead.GetBool() && Main.AllAlivePlayerControls.Length >= Options.SpawnAdditionalRefugeeMinAlivePlayers.GetInt() && !CustomRoles.Refugee.RoleExist(countDead: true) && !Main.AllAlivePlayerControls.Any(x => x.PlayerId != deadPlayer.PlayerId && (x.Is(CustomRoleTypes.Impostor) || (x.IsNeutralKiller() && Options.SpawnAdditionalRefugeeWhenNKAlive.GetBool())))) + { + PlayerControl[] ListToChooseFrom = Options.UsePets.GetBool() ? Main.AllAlivePlayerControls.Where(x => x.PlayerId != deadPlayer.PlayerId && x.Is(CustomRoleTypes.Crewmate)).ToArray() : Main.AllAlivePlayerControls.Where(x => x.PlayerId != deadPlayer.PlayerId && x.Is(CustomRoleTypes.Crewmate) && x.GetCustomRole().GetRoleTypes() == RoleTypes.Impostor).ToArray(); + + if (ListToChooseFrom.Length > 0) + { + var pc = ListToChooseFrom.RandomElement(); + pc.RpcSetCustomRole(CustomRoles.Refugee); + pc.SetKillCooldown(); + Logger.Warn($"{pc.GetRealName()} is now a Refugee since all Impostors are dead", "Add Refugee"); + } + else Logger.Msg("No Player to change to Refugee.", "Add Refugee"); + } + } + public static void SendMessage(string text, byte sendTo = byte.MaxValue, string title = "") { if (!AmongUsClient.Instance.AmHost) return; @@ -1982,7 +2000,6 @@ public static class Utils if (Pelican.IsEaten(seer.PlayerId)) SelfName = $"{ColorString(GetRoleColor(CustomRoles.Pelican), GetString("EatenByPelican"))}"; if (Deathpact.IsInActiveDeathpact(seer)) SelfName = Deathpact.GetDeathpactString(seer); - if (NameNotifyManager.GetNameNotify(seer, out var name)) SelfName = name; // Devourer if (Devourer.HideNameOfConsumedPlayer.GetBool() && Devourer.playerIdList.Any(x => Main.PlayerStates[x].Role is Devourer { IsEnable: true } dv && dv.PlayerSkinsCosumed.Contains(seer.PlayerId)) && !CamouflageIsForMeeting) @@ -1993,6 +2010,8 @@ public static class Utils GameMode2: + if (NameNotifyManager.GetNameNotify(seer, out var name)) SelfName = name; + switch (Options.CurrentGameMode) { case CustomGameMode.SoloKombat: @@ -2008,7 +2027,7 @@ public static class Utils break; } - SelfName += SelfSuffix.ToString() == string.Empty ? string.Empty : $"\r\n {SelfSuffix}"; + SelfName += SelfSuffix.ToString() == string.Empty ? string.Empty : $"\r\n{SelfSuffix}"; if (!isForMeeting) SelfName += "\r\n"; seer.RpcSetNamePrivate(SelfName, true, force: NoCache); @@ -2128,7 +2147,7 @@ public static class Utils (target.Is(CustomRoles.Mayor) && Mayor.MayorRevealWhenDoneTasks.GetBool() && target.GetTaskState().IsTaskFinished) || (seer.Is(CustomRoleTypes.Crewmate) && target.Is(CustomRoles.Marshall) && target.GetTaskState().IsTaskFinished) || (Main.PlayerStates[target.PlayerId].deathReason == PlayerState.DeathReason.Vote && Options.SeeEjectedRolesInMeeting.GetBool()) || - CustomTeamManager.AreInSameCustomTeam(seer.PlayerId, target.PlayerId) && CustomTeamManager.GetSettingForPlayerTeam(seer.PlayerId, "KnowRoles") || + CustomTeamManager.AreInSameCustomTeam(seer.PlayerId, target.PlayerId) && CustomTeamManager.IsSettingEnabledForPlayerTeam(seer.PlayerId, "KnowRoles") || Main.PlayerStates.Values.Any(x => x.Role.KnowRole(seer, target)) || Markseeker.PlayerIdList.Any(x => Main.PlayerStates[x].Role is Markseeker { IsEnable: true, TargetRevealed: true } ms && ms.MarkedId == target.PlayerId) || Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.MoveAndStop or CustomGameMode.HotPotato || diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index 958a275f0..13e2a56d9 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -235,7 +235,7 @@ class GameEndChecker .Where(x => x.Team != null) .GroupBy(x => x.Team) .ToDictionary(x => x.Key, x => x.Select(y => y.Player.PlayerId)) - .DoIf(x => !CustomTeamManager.GetSettingForTeam(x.Key, "WinWithOriginalTeam"), x => WinnerIds.ExceptWith(x.Value)); + .DoIf(x => !CustomTeamManager.IsSettingEnabledForTeam(x.Key, "WinWithOriginalTeam"), x => WinnerIds.ExceptWith(x.Value)); } if ((WinnerTeam == CustomWinner.Lovers || WinnerIds.Any(x => Main.PlayerStates[x].SubRoles.Contains(CustomRoles.Lovers))) && Main.LoversPlayers.All(x => x.IsAlive())) @@ -267,6 +267,8 @@ class GameEndChecker } } } + + WinnerIds.RemoveWhere(x => Main.PlayerStates[x].MainRole == CustomRoles.Shifter); } Camouflage.BlockCamouflage = true; diff --git a/Patches/ExilePatch.cs b/Patches/ExilePatch.cs index 1c69d3754..5f5fa6e2f 100644 --- a/Patches/ExilePatch.cs +++ b/Patches/ExilePatch.cs @@ -1,6 +1,5 @@ using System.Linq; using AmongUs.Data; -using AmongUs.GameOptions; using EHR.Roles.AddOns.Crewmate; using EHR.Roles.AddOns.Impostor; using EHR.Roles.Crewmate; @@ -134,20 +133,7 @@ class ExileControllerWrapUpPatch if (map != null) Main.AllAlivePlayerControls.Do(map.RandomTeleport); } - if (exiled != null && Options.SpawnAdditionalRefugeeOnImpsDead.GetBool() && Main.AllAlivePlayerControls.Length >= Options.SpawnAdditionalRefugeeMinAlivePlayers.GetInt() && !CustomRoles.Refugee.RoleExist(countDead: true) && !Main.AllAlivePlayerControls.Any(x => x.PlayerId != exiled.PlayerId && (x.Is(CustomRoleTypes.Impostor) || (x.IsNeutralKiller() && Options.SpawnAdditionalRefugeeWhenNKAlive.GetBool())))) - { - PlayerControl[] ListToChooseFrom = Options.UsePets.GetBool() ? Main.AllAlivePlayerControls.Where(x => x.PlayerId != exiled.PlayerId && x.Is(CustomRoleTypes.Crewmate)).ToArray() : Main.AllAlivePlayerControls.Where(x => x.PlayerId != exiled.PlayerId && x.Is(CustomRoleTypes.Crewmate) && x.GetCustomRole().GetRoleTypes() == RoleTypes.Impostor).ToArray(); - - if (ListToChooseFrom.Length > 0) - { - var index = IRandom.Instance.Next(0, ListToChooseFrom.Length); - var pc = ListToChooseFrom[index]; - pc.RpcSetCustomRole(CustomRoles.Refugee); - pc.SetKillCooldown(); - Logger.Warn($"{pc.GetRealName()} is now a Refugee since all Impostors are dead", "Add Refugee"); - } - else Logger.Msg("No Player to change to Refugee.", "Add Refugee"); - } + Utils.CheckAndSpawnAdditionalRefugee(exiled); FallFromLadder.Reset(); Utils.CountAlivePlayers(true); diff --git a/Patches/HudSpritePatch.cs b/Patches/HudSpritePatch.cs index 0ed0202f8..8fb2e963a 100644 --- a/Patches/HudSpritePatch.cs +++ b/Patches/HudSpritePatch.cs @@ -58,6 +58,9 @@ public static class HudSpritePatch switch (player.GetCustomRole()) { + case CustomRoles.Shifter: + newKillButton = CustomButton.Get("Swap"); + break; case CustomRoles.Changeling: newAbilityButton = CustomButton.Get("GlitchMimic"); break; diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index 4851f9422..ee0499321 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -810,7 +810,7 @@ class MeetingHudStartPatch (pc.Is(CustomRoles.Mayor) && Mayor.MayorRevealWhenDoneTasks.GetBool() && pc.GetTaskState().IsTaskFinished) || (pc.Is(CustomRoles.Marshall) && PlayerControl.LocalPlayer.Is(CustomRoleTypes.Crewmate) && pc.GetTaskState().IsTaskFinished) || (Main.PlayerStates[pc.PlayerId].deathReason == PlayerState.DeathReason.Vote && Options.SeeEjectedRolesInMeeting.GetBool()) || - CustomTeamManager.AreInSameCustomTeam(pc.PlayerId, PlayerControl.LocalPlayer.PlayerId) && CustomTeamManager.GetSettingForPlayerTeam(pc.PlayerId, "KnowRoles") || + CustomTeamManager.AreInSameCustomTeam(pc.PlayerId, PlayerControl.LocalPlayer.PlayerId) && CustomTeamManager.IsSettingEnabledForPlayerTeam(pc.PlayerId, "KnowRoles") || Main.PlayerStates.Values.Any(x => x.Role.KnowRole(PlayerControl.LocalPlayer, pc)) || Markseeker.PlayerIdList.Any(x => Main.PlayerStates[x].Role is Markseeker { IsEnable: true, TargetRevealed: true } ms && ms.MarkedId == pc.PlayerId) || PlayerControl.LocalPlayer.IsRevealedPlayer(pc) || diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index f363bd8cf..80c4e495f 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -299,7 +299,7 @@ class CheckMurderPatch if (!AmongUsClient.Instance.AmHost) return false; if (target == null) target = killer; - if (CustomTeamManager.AreInSameCustomTeam(killer.PlayerId, target.PlayerId) && !CustomTeamManager.GetSettingForPlayerTeam(killer.PlayerId, "KillEachOther")) return false; + if (CustomTeamManager.AreInSameCustomTeam(killer.PlayerId, target.PlayerId) && !CustomTeamManager.IsSettingEnabledForPlayerTeam(killer.PlayerId, "KillEachOther")) return false; if (killer.Is(CustomRoles.Jackal) && target.Is(CustomRoles.Sidekick) && !Options.JackalCanKillSidekick.GetBool()) return false; if (killer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Jackal) && !Options.SidekickCanKillJackal.GetBool()) return false; diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index da724bd1c..3772d9c50 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -3410,6 +3410,11 @@ "BenefactorTargetGotShieldNotify": "You got a temporary shield as the task\\nyou completed was marked by the Benefactor!", "BenefactorTaskMarked": "Task marked", "TaskMarkPerRound": "Maximum Number of Tasks Marked Each Round", + "Shifter": "Shifter", + "ShifterInfo": "Steal Someone's Role", + "ShifterInfoLong": "(Neutrals):\\nAs the Shifter, you must steal someone's role by using your kill button on them. Depending on the settings, you may also steal their progress in their role and ability uses, and their add-ons.\\nIf you have this role when the game ends, you lose!", + "Shifter.StealAddons": "Steal Add-ons", + "Shifter.StealProgress": "Steal Progress", "Rogue": "Rogue", "RogueInfo": "Complete Objectives, Get Rewards", "RogueInfoLong": "(Neutral):\\nAs the Rogue, you get an objective with a reward each round. You can optionally complete the objective to get the reward.", diff --git a/Roles/Neutral/Shifter.cs b/Roles/Neutral/Shifter.cs new file mode 100644 index 000000000..9487e9b6c --- /dev/null +++ b/Roles/Neutral/Shifter.cs @@ -0,0 +1,101 @@ +using System.Linq; +using AmongUs.GameOptions; +using EHR.Modules; +using HarmonyLib; + +namespace EHR.Neutral +{ + public class Shifter : RoleBase + { + private const int Id = 644400; + public static bool On; + + private static OptionItem KillCooldown; + private static OptionItem CanVent; + private static OptionItem HasImpostorVision; + private static OptionItem StealAddons; + private static OptionItem StealProgress; + + public override bool IsEnable => On; + + public static void SetupCustomOption() + { + Options.SetupRoleOptions(Id, TabGroup.NeutralRoles, CustomRoles.Shifter); + KillCooldown = FloatOptionItem.Create(Id + 2, "AbilityCooldown", new(0f, 180f, 0.5f), 15f, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Shifter]) + .SetValueFormat(OptionFormat.Seconds); + CanVent = BooleanOptionItem.Create(Id + 3, "CanVent", true, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Shifter]); + HasImpostorVision = BooleanOptionItem.Create(Id + 4, "ImpostorVision", true, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Shifter]); + StealAddons = BooleanOptionItem.Create(Id + 5, "Shifter.StealAddons", true, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Shifter]); + StealProgress = BooleanOptionItem.Create(Id + 6, "Shifter.StealProgress", true, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Shifter]); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + + if (!AmongUsClient.Instance.AmHost) return; + if (!Main.ResetCamPlayerList.Contains(playerId)) + Main.ResetCamPlayerList.Add(playerId); + } + + public override void SetKillCooldown(byte id) => Main.AllPlayerKillCooldown[id] = KillCooldown.GetFloat(); + public override void ApplyGameOptions(IGameOptions opt, byte id) => opt.SetVision(HasImpostorVision.GetBool()); + public override bool CanUseImpostorVentButton(PlayerControl pc) => CanVent.GetBool(); + public override bool CanUseKillButton(PlayerControl pc) => true; + + public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) + { + if (!base.OnCheckMurder(killer, target)) return false; + + killer.RpcSetCustomRole(target.GetCustomRole()); + + if (StealProgress.GetBool()) + { + Main.PlayerStates[killer.PlayerId].Role = Main.PlayerStates[target.PlayerId].Role; + killer.SetAbilityUseLimit(target.GetAbilityUseLimit()); + } + + if (StealAddons.GetBool()) + { + var killerSubRoles = killer.GetCustomSubRoles(); + var targetSubRoles = target.GetCustomSubRoles(); + if (targetSubRoles.Count > 0) + { + killer.RpcSetCustomRole(targetSubRoles[0], replaceAllAddons: true); + targetSubRoles.Skip(1).Do(x => killer.RpcSetCustomRole(x)); + } + + if (killerSubRoles.Count > 0) + { + target.RpcSetCustomRole(killerSubRoles[0], replaceAllAddons: true); + killerSubRoles.Skip(1).Do(x => target.RpcSetCustomRole(x)); + } + } + + // ------------------------------------------------------------------------------------------ + + target.RpcSetCustomRole(CustomRoles.Shifter); + Main.AbilityUseLimit.Remove(target.PlayerId); + Utils.SendRPC(CustomRPC.RemoveAbilityUseLimit, target.PlayerId); + target.SyncSettings(); + target.SetKillCooldown(); + + // ------------------------------------------------------------------------------------------ + + Utils.NotifyRoles(SpecifyTarget: killer); + Utils.NotifyRoles(SpecifyTarget: target); + + return false; + } + } +} \ No newline at end of file diff --git a/main.cs b/main.cs index 9e84849ca..5aea9246c 100644 --- a/main.cs +++ b/main.cs @@ -445,6 +445,7 @@ public class Main : BasePlugin { CustomRoles.PlagueDoctor, "#ff6633" }, { CustomRoles.Postman, "#00b893" }, { CustomRoles.SchrodingersCat, "#616161" }, + { CustomRoles.Shifter, "#777777" }, { CustomRoles.Impartial, "#4287f5" }, { CustomRoles.Predator, "#c73906" }, { CustomRoles.Reckless, "#6e000d" },