using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using AmongUs.Data; using AmongUs.GameOptions; using HarmonyLib; using InnerNet; using MiraAPI.GameOptions; using MiraAPI.HnsReimplemented; using MiraAPI.HnsReimplemented.Options; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using PowerTools; using UnityEngine; namespace MiraAPI.GameModes; /// /// The vanilla Hide and Seek game mode, ported to Mira. /// [MiraIgnore] public class HideAndSeekMode : AbstractGameMode { /// public override string Name => "MiraApi.Gamemode.HideAndSeek"; /// public override string Description => "MiraApi.Gamemode.HideAndSeek.Description"; /// // TODO: Once the Hide n Seek port is ready for release, we will remove this. public override bool HideMode => !MiraApiPlugin.IsDevBuild; /// public override Color Color { get; } = new Color32(255, 88, 90, 255); /// public override LoadableAsset Icon => MiraAssets.HnSGamemodeIcon; /// public override bool ShowGameModeIntroCutscene => true; /// public override bool GameModeBodyTypeOverride => true; /// public override bool ShowNormalGameSettings => false; /// public override bool ShowNormalRoleSettings => false; /// public override float DefaultImpostorKillCooldown => 1f; /// public override bool ShowTaskBar => false; /// public override bool CanReport(DeadBody body) { return false; } /// public override bool ShouldShowSabotageMap(MapBehaviour map) { return false; } private static int ImpostorPlayerID() { return OptionGroupSingleton.Instance.SelectedSeeker.Value; } private static bool HasImpostorPlayerID() { return ImpostorPlayerID() > -1; } private static bool ValidateImpostorPlayerID(List players) { return HasImpostorPlayerID() && players.Find(p => p.PlayerId == ImpostorPlayerID()) != null; } /// public override void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) { runOriginal = false; CppCollections.List list = new(); AmongUsClient.Instance.GetAllClients(list); List list2 = [.. list.ToArray() .Where(c => c.Character != null && c.Character.Data != null && !c.Character.Data.Disconnected && !c.Character.Data.IsDead).OrderBy(c => c.Id).Select(c => c.Character.Data)]; foreach (var networkedPlayerInfo in GameData.Instance.AllPlayers) { if (networkedPlayerInfo.Object != null && networkedPlayerInfo.Object.isDummy) { list2.Add(networkedPlayerInfo); } } var currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions; var adjustedNumImpostors = GameOptionsManager.Instance.CurrentGameOptions.GetAdjustedNumImpostors(list2.Count); AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, Math.Max(adjustedNumImpostors, 1), RoleTypes.Impostor); AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, RoleTypes.Engineer); } private static void AddGuaranteedRoles(IEnumerable guaranteedRoles, IGameOptions opts, List list) { var roleOptions = opts.RoleOptions; foreach (var roleAssignmentData in guaranteedRoles.Select(x => new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100))) { while (true) { var count = roleAssignmentData.Count; roleAssignmentData.Count = count - 1; if (count <= 0) break; list.Add(roleAssignmentData.Role.Role); } } } private static void AddPotentialRoles(IEnumerable source, IGameOptions opts, List list) { var roleOptions = opts.RoleOptions; var potentialRoles = source.Where(x => !x.IsDead).Select(role => new RoleManager.RoleAssignmentData( role, roleOptions.GetNumPerGame(role.Role), roleOptions.GetChancePerGame(role.Role))).ToList(); list.Clear(); foreach (var roleData in potentialRoles) { for (var i = 0; i < roleData.Count; i++) { if (HashRandom.Next(101) < roleData.Chance) { list.Add(roleData.Role.Role); } } } } private static void AddFallbackRoles(List list, int targetPlayerCount, int teamMax, int rolesAssigned, RoleTypes basicRole) { while (list.Count < targetPlayerCount && list.Count + rolesAssigned < teamMax) { list.Add(basicRole); } } private static void AssignRolesForTeam( List players, IGameOptions opts, RoleTeamTypes team, int teamMax, RoleTypes defaultRole) { Info($"Hide And Seek Mode - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}"); var num = 0; var roleOptions = opts.RoleOptions; var source = RoleManager.Instance.AllRoles.ToArray() .Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) && CustomRoleUtils.CanSpawnOnCurrentMode(role)) .ToList(); var assignmentData = source.Where(x => !x.IsDead).Select(role => new RoleManager.RoleAssignmentData( role, roleOptions.GetNumPerGame(role.Role), roleOptions.GetChancePerGame(role.Role))).ToList(); var source2 = CustomRoleUtils.GetPossibleRoles(assignmentData, x => x.Chance == 100); var guaranteedRoles = source.Where(x => source2.Contains(((ushort)x.Role, 100))).ToList(); List list = []; switch (team) { case RoleTeamTypes.Crewmate: Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); AddGuaranteedRoles(guaranteedRoles, opts, list); Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment"); AssignRolesFromList(players, teamMax, list, ref num); AddPotentialRoles(source, opts, list); Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Potential Assignment"); AssignRolesFromList(players, teamMax, list, ref num); const RoleTypes basicCrewRole = RoleTypes.Engineer; AddFallbackRoles(list, players.Count, teamMax, num, basicCrewRole); AssignRolesFromList(players, teamMax, list, ref num); Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Fallback Assignment"); break; case RoleTeamTypes.Impostor: Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); var newImpostors = new List(); if (HasImpostorPlayerID() && ValidateImpostorPlayerID(players) && !AmongUsClient.Instance.IsGamePublic) { // Specified Seeker var networkedPlayerInfo = players.First(p => p.PlayerId == ImpostorPlayerID()); players.Remove(networkedPlayerInfo); newImpostors.Add(networkedPlayerInfo); Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); } else { // Random Seeker var num2 = 0; while (num2 < teamMax && players.Count > 0) { PseudoRandomList pseudoRandomList = new(AmongUsClient.Instance.GameId); players.Do(pseudoRandomList.Add); for (var i = 0; i < GameData.RoundsPlayedInSession; i++) { pseudoRandomList.PickRandom(); } var networkedPlayerInfo = pseudoRandomList.PickRandom(); players.Remove(networkedPlayerInfo); newImpostors.Add(networkedPlayerInfo); num2++; Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); } } Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment"); AddGuaranteedRoles(guaranteedRoles, opts, list); AssignRolesFromList(newImpostors, teamMax, list, ref num); AddPotentialRoles(source, opts, list); AssignRolesFromList(newImpostors, teamMax, list, ref num); const RoleTypes basicImpRole = RoleTypes.Impostor; AddFallbackRoles(list, newImpostors.Count, teamMax, num, basicImpRole); AssignRolesFromList(newImpostors, teamMax, list, ref num); break; default: throw new ArgumentOutOfRangeException(nameof(team), team, null); } } private static void AssignRolesFromList(List players, int teamMax, List roleList, ref int rolesAssigned) { while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax) { var index = HashRandom.FastNext(roleList.Count); var roleType = roleList[index]; roleList.RemoveAt(index); var index2 = HashRandom.FastNext(players.Count); players[index2].Object.RpcSetRole(roleType); players.RemoveAt(index2); rolesAssigned++; } } /// public override IEnumerator IntroCutscene(IntroCutscene introCutscene) { SoundManager.Instance.PlaySound(introCutscene.IntroStinger, false); Logger.GlobalInstance.Info("IntroCutscene :: CoBegin() :: Game Mode: Hide and Seek (MiraAPI)"); introCutscene.LogPlayerRoleData(); introCutscene.HideAndSeekPanels.SetActive(true); if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) { introCutscene.CrewmateRules.SetActive(false); introCutscene.ImpostorRules.SetActive(true); } else { introCutscene.CrewmateRules.SetActive(true); introCutscene.ImpostorRules.SetActive(false); } introCutscene.ImpostorName.gameObject.SetActive(true); introCutscene.ImpostorTitle.gameObject.SetActive(true); introCutscene.BackgroundBar.enabled = false; introCutscene.TeamTitle.gameObject.SetActive(false); var impostor = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(x => x.Data.Role.IsImpostor); if (impostor == null) { Logger.GlobalInstance.Error("IntroCutscene :: CoBegin() :: impostor is NULL"); } GameManager.Instance.SetSpecialCosmetics(impostor); introCutscene.ImpostorName.text = impostor != null ? impostor.Data.PlayerName : "???"; yield return new WaitForSecondsRealtime(0.1f); if (impostor != null) { introCutscene.ImpostorTitle.text = impostor.Data.Role.GetRoleName(); } PoolablePlayer? playerSlot = null; if (impostor != null) { playerSlot = introCutscene.CreatePlayer(1, 1, impostor.Data, false); playerSlot.SetBodyType(PlayerBodyTypes.Normal); playerSlot.SetFlipX(false); playerSlot.transform.localPosition = introCutscene.impostorPos; playerSlot.transform.localScale = Vector3.one * introCutscene.impostorScale; } yield return ShipStatus.Instance.CosmeticsCache.PopulateFromPlayers(); yield return new WaitForSecondsRealtime(6f); if (playerSlot != null) { playerSlot.gameObject.SetActive(false); } introCutscene.HideAndSeekPanels.SetActive(false); introCutscene.CrewmateRules.SetActive(false); introCutscene.ImpostorRules.SetActive(false); HnsMusicHandler.Instance.StartMusicWithIntro(); var hideTimer = 10f; if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) { introCutscene.HideAndSeekTimerText.gameObject.SetActive(true); PoolablePlayer poolablePlayer; AnimationClip anim; if (AprilFoolsMode.ShouldHorseAround()) { poolablePlayer = introCutscene.HorseWrangleVisualSuit; poolablePlayer.gameObject.SetActive(true); poolablePlayer.SetBodyType(PlayerBodyTypes.Seeker); anim = introCutscene.HnSSeekerSpawnHorseAnim; introCutscene.HorseWrangleVisualPlayer.SetBodyType(PlayerBodyTypes.Normal); introCutscene.HorseWrangleVisualPlayer.UpdateFromPlayerData( PlayerControl.LocalPlayer.Data, PlayerControl.LocalPlayer.CurrentOutfitType, PlayerMaterial.MaskType.None, false); } else if (AprilFoolsMode.ShouldLongAround()) { poolablePlayer = introCutscene.HideAndSeekPlayerVisual; poolablePlayer.gameObject.SetActive(true); poolablePlayer.SetBodyType(PlayerBodyTypes.LongSeeker); anim = introCutscene.HnSSeekerSpawnLongAnim; } else { poolablePlayer = introCutscene.HideAndSeekPlayerVisual; poolablePlayer.gameObject.SetActive(true); poolablePlayer.SetBodyType(PlayerBodyTypes.Seeker); anim = introCutscene.HnSSeekerSpawnAnim; } poolablePlayer.SetBodyCosmeticsVisible(false); poolablePlayer.UpdateFromPlayerData( PlayerControl.LocalPlayer.Data, PlayerControl.LocalPlayer.CurrentOutfitType, PlayerMaterial.MaskType.None, false); var component = poolablePlayer.GetComponent(); poolablePlayer.gameObject.SetActive(true); poolablePlayer.ToggleName(false); component.Play(anim); while (hideTimer > 0f) { introCutscene.HideAndSeekTimerText.text = Mathf.RoundToInt(hideTimer).ToString(CultureInfo.InvariantCulture); hideTimer -= Time.deltaTime; yield return null; } } else { HideAndSeekHudHelper.Instance.HideCountdown = hideTimer; if (AprilFoolsMode.ShouldHorseAround()) { if (impostor != null) { impostor.AnimateCustom(introCutscene.HnSSeekerSpawnHorseInGameAnim); } } else if (AprilFoolsMode.ShouldLongAround()) { if (impostor != null) { impostor.AnimateCustom(introCutscene.HnSSeekerSpawnLongInGameAnim); } } else if (impostor != null) { impostor.AnimateCustom(introCutscene.HnSSeekerSpawnAnim); impostor.cosmetics.SetBodyCosmeticsVisible(false); } } ShipStatus.Instance.StartSFX(); HnsMusicHandler.Instance.OnGameStart(); HnsDangerMeter.Instance.OnGameStart(); UnityEngine.Object.Destroy(introCutscene.gameObject); } /// public override void Initialize() { deadPlayerCount = 0; ShipStatus.Instance.BreakEmergencyButton(); PlayerControl.LocalPlayer.SetKillTimer(0.01f); if (!HudManager.InstanceExists) return; HudManager.Instance.gameObject.AddComponent(); HudManager.Instance.gameObject.AddComponent(); HudManager.Instance.gameObject.AddComponent(); } /// public override MapOptions GetMapOptions() { MapOptions mapOptions = new() { Mode = MapOptions.Modes.Normal, }; if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && HideAndSeekHudHelper.Instance.SeekerAdminMapEnabled(PlayerControl.LocalPlayer)) { mapOptions.Mode = MapOptions.Modes.CountOverlay; mapOptions.AllowMovementWhileMapOpen = true; mapOptions.IncludeDeadBodies = false; mapOptions.ShowLivePlayerPosition = false; } return mapOptions; } /// public override void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) { runOriginal = false; var players = Helpers.GetAlivePlayers(); if (!players.Any(x => x.Data.Role.IsImpostor)) { instance.Manager.RpcEndGame(GameOverReason.ImpostorDisconnect, !DataManager.Player.Ads.HasPurchasedAdRemoval); } if (players.Any(x => !x.Data.Role.IsImpostor)) { if (HideAndSeekHudHelper.Instance.AllTimersExpired()) { instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_CrewmatesByTimer, !DataManager.Player.Ads.HasPurchasedAdRemoval); } return; } instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_ImpostorsByKills, !DataManager.Player.Ads.HasPurchasedAdRemoval); } /// public override PlayerBodyTypes GetBodyType(PlayerControl player) { if (player == null || player.Data == null || player.Data.Role == null) { if (AprilFoolsMode.ShouldHorseAround()) { return PlayerBodyTypes.Horse; } if (AprilFoolsMode.ShouldLongAround()) { return PlayerBodyTypes.Long; } return PlayerBodyTypes.Normal; } if (AprilFoolsMode.ShouldHorseAround()) { return player.Data.Role.IsImpostor ? PlayerBodyTypes.Normal : PlayerBodyTypes.Horse; } if (AprilFoolsMode.ShouldLongAround()) { return player.Data.Role.IsImpostor ? PlayerBodyTypes.LongSeeker : PlayerBodyTypes.Long; } return player.Data.Role.IsImpostor ? PlayerBodyTypes.Seeker : PlayerBodyTypes.Normal; } /// public override void UpdateTaskPanel(TaskPanelBehaviour instance) { instance.background.transform.localScale = (instance.taskText.textBounds.size.x > 0f) ? new Vector3(instance.taskText.textBounds.size.x + 0.2f, instance.taskText.textBounds.size.y + 0.2f, 1f) : Vector3.zero; var vector = instance.background.sprite.bounds.extents; vector.y = -vector.y; vector = vector.Mul(instance.background.transform.localScale); instance.background.transform.localPosition = vector; var vector2 = instance.tab.sprite.bounds.extents; vector2 = vector2.Mul(instance.tab.transform.localScale); vector2.y = -vector2.y; vector2.x += vector.x * 2f; instance.tab.transform.localPosition = vector2; if (GameManager.Instance == null) { return; } var yPos = 1.6f; var xPos = -instance.background.sprite.bounds.size.x * instance.background.transform.localScale.x; instance.closedPosition = new Vector3(xPos, yPos, instance.closedPosition.z); instance.openPosition = new Vector3(instance.openPosition.x, yPos, instance.openPosition.z); instance.timer = instance.open ? Mathf.Min(1f, instance.timer + (Time.deltaTime / instance.animationTimeSeconds)) : Mathf.Max(0f, instance.timer - (Time.deltaTime / instance.animationTimeSeconds)); Vector3 relativePos = new( Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer), yPos, instance.openPosition.z); instance.transform.localPosition = AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos); } private int deadPlayerCount; /// public override void OnPlayerDeath(PlayerControl player, bool assignGhostRole) { base.OnPlayerDeath(player, assignGhostRole); HudManager.Instance.NotifyOfDeath(); var popup = GameManagerCreator.Instance.HideAndSeekManagerPrefab.DeathPopupPrefab; deadPlayerCount++; var item = UnityEngine.Object.Instantiate(popup, HudManager.Instance.transform.parent); item.Show(player, deadPlayerCount); } }